Menu ▾ ▴

#1280 OCCURS DYNAMIC, only warned as not implemented, compiles to a zero-byte record and stores outside it

GC 3.x
open
nobody
None
5 - default
2026-10-30
2026-10-30
No

cobc accepts OCCURS DYNAMIC with only a warning, but gives the table no storage at all. Every subscripted reference then reads or writes outside the record, and the program runs on without any error. A program that uses a dynamic table can silently overwrite its own data.

To reproduce

occdyn2.cob (attached) moves values into two elements of a dynamic table:

IDENTIFICATION DIVISION.
PROGRAM-ID. OCCDYN2.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 I PIC 9 VALUE 1.
01 J PIC 9 VALUE 2.
01 T.
   05 E PIC X(3) OCCURS DYNAMIC.
01 W PIC X(3) VALUE "WWW".
PROCEDURE DIVISION.
MOVE "AAA" TO E(I)
MOVE "BBB" TO E(J)
DISPLAY E(I) E(J) W
STOP RUN.
cobc -std=cobol2014 -fformat=free -x occdyn2.cob && ./occdyn2 | od -c

What happens

Compiling gives a warning, then a zero-byte record. 3.2.0, exit 0:

occdyn2.cob:8: warning: OCCURS DYNAMIC is not implemented [-Wpending]
    6 | 01 J PIC 9 VALUE 2.
    7 | 01 T.
    8 >    05 E PIC X(3) OCCURS DYNAMIC.
    9 | 01 W PIC X(3) VALUE "WWW".
   10 | PROCEDURE DIVISION.
/tmp/cob7165_0.c: In function ‘OCCDYN2_’:
/tmp/cob7165_0.c:103:3: warning: ‘memset’ writing 3 bytes into a region of size 0 overflows the destination [-Wstringop-overflow=]
  103 |   memset (b_3 + 3LL * ((cob_s64_t)(cob_get_numdisp (b_1, 1)) - 1), 65, 3);
      |   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /tmp/cob7165_0.c:58:
/tmp/cob7165_0.c.l.h:36:17: note: destination object ‘b_3’ of size 0
   36 | static cob_u8_t b_3[0] __attribute__((aligned));        /* T */
      |                 ^~~
/tmp/cob7165_0.c:106:3: warning: ‘memset’ writing 3 bytes into a region of size 0 overflows the destination [-Wstringop-overflow=]
  106 |   memset (b_3 + 3LL * ((cob_s64_t)(cob_get_numdisp (b_2, 1)) - 1), 66, 3);
      |   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/tmp/cob7165_0.c.l.h:36:17: note: destination object ‘b_3’ of size 0
   36 | static cob_u8_t b_3[0] __attribute__((aligned));        /* T */
      |                 ^~~

The default dialect gives the same. 3.3-dev r5751, the same command, exit 0: the same output apart from the temporary file names.

The program then stores outside the record and exits 0. 3.2.0, ./occdyn2 | od -c:

0000000   B   B   B  \0  \0  \0   W   W   W  \n
0000012

3.3-dev r5751 gives byte-identical output, run exit 0.

With -debug the first MOVE is trapped. 3.2.0, cobc -std=cobol2014 -fformat=free -debug -x occdyn2.cob && ./occdyn2, compile output as above (line numbers shifted), run exit 1:

libcob: occdyn2.cob:11: error: subscript of 'E' out of bounds: 1
note: maximum subscript for 'E': 0

 Last statement of "OCCDYN2" was MOVE at line 11 of occdyn2.cob
    ENTRY OCCDYN2 at occdyn2.cob:11
 Started by ./occdyn2

3.3-dev r5751 gives the same output, run exit 1.

What should happen

cobc should stop with a compile-time error for OCCURS DYNAMIC while it is not implemented, so that no executable is produced that stores outside its record. (-Werror=pending gives this on both versions, but only when the user asks for it.)

This is not a conformance report. Dynamic-capacity tables are an optional language element in ISO/IEC 1989:2023, and the 2014 edition lists them as optional too. GnuCOBOL does not claim them.

  • §A.4.4 item 2: "OCCURS clause, format 4: dynamic-capacity-table".
  • §4.2.7: "Language elements that an implementor may, but need not, implement are listed in A.4, Optional language element list."

For reference, an implementation that does claim them would print AAABBBWWW. §8.5.1.9.3 says that when an element is referenced as a receiving item beyond the current capacity, "a new element is automatically created and the capacity of the table is increased to the value given by the subscript".

Why it happens

In the OCCURS DYNAMIC arm of occurs_clause in cobc/parser.y, the unimplemented feature is only a -Wpending warning, CB_PENDING ("OCCURS DYNAMIC"). Without a TO phrase this arm sets occurs_max to 0, so compute_size gives the table no storage and every subscripted reference lands outside the record. With a TO phrase the table is a fixed table of TO elements, which is not the dynamic-capacity behavior either.

Proposed fix

The attached occurs-dynamic-silent.patch is against 3.3-dev r5751 and applies with patch -p1 at the top of the tree. It replaces CB_PENDING ("OCCURS DYNAMIC") with cb_error (_("%s is not implemented"), "OCCURS DYNAMIC"). The message text stays the same and reuses the existing translated string, and an unimplemented intrinsic function is already an error in the same words (FUNCTION '%s' is not implemented, cobc/tree.c). The patch refuses both forms, with and without a TO phrase.

With the patch, built-in-tree cobc, no executable is produced:

$ cobc -std=cobol2014 -fformat=free -x occdyn2.cob
occdyn2.cob:8: error: OCCURS DYNAMIC is not implemented
    6 | 01 J PIC 9 VALUE 2.
    7 | 01 T.
    8 >    05 E PIC X(3) OCCURS DYNAMIC.
    9 | 01 W PIC X(3) VALUE "WWW".
   10 | PROCEDURE DIVISION.
(exit 1)

The default dialect gives the same output, exit 1.

make check on 3.3-dev r5751 with the patch: 1,373 behaved as expected, 9 skipped, 0 failed, as unpatched (1,372 + the new test); the new test 163 "OCCURS DYNAMIC" (tests/testsuite.src/syn_occurs.at) passes. It checks the error for OCCURS DYNAMIC with and without CAPACITY IN ... FROM ... TO, with the default dialect and -std=cobol2014. No existing test used OCCURS DYNAMIC.

  • feature-requests #60: asks for dynamic-capacity tables and notes that they are PENDING in the parser. It does not mention the zero-byte record.

Details

GnuCOBOL 3.2.0 (Ubuntu gnucobol3 3.2-4build1) and 3.3-dev r5751 (CI snapshot 3.3-dev.138 of 2026-10-29), x86_64 Linux, both re-run 2026-10-30. On 3.2.0 the Ubuntu C compiler step also prints <command-line>: warning: ‘_FORTIFY_SOURCE’ redefined and its note; that is the distribution's build setting.

2 Attachments

Discussion


Log in to post a comment.