Menu

#1247 string overflow in runtime_err_str via cob_load_collation

GC 3.x
accepted
nobody
7
4 days ago
4 days ago
Zian Luo
No

Unbounded vsprintf into 1024-byte runtime_err_str overflows on long collation name in cob_load_collation

Summary

cob_load_collation() (libcob/cconv.c) reports a missing translation-table file by passing the caller-supplied col_name verbatim to cob_runtime_error(). That message is formatted into the fixed 1024-byte global buffer runtime_err_str via unbounded vsprintf() (libcob/common.c:9058), so a collation name longer than ~1010 bytes overflows the buffer. The API places no length limit on col_name, and "file unknown → return -1" is the documented behaviour, so the overflow in the error path is a library bug, not API misuse.

Version

$ git describe --tags --always
a672a26b
  • Repo: https://github.com/OCamlPro/gnucobol
  • Branch: origin/gnucobol-3.x (tip; gnucobol-3.2-rc2 is the most recent tag, 330 commits behind HEAD — no tag describes a672a26b)
  • Commit: a672a26b52b594bd0ebfdcfe0200613c572018d5 (2026-06-09, "Fix handling of some special contexts, and provide room for more")

Description

cob_load_collation() resolves the table name to a filename and, when fopen() fails, reports the error using the original (untruncated) col_name:

// libcob/cconv.c:168-172
f = fopen (filename, "r");
if (f == NULL) {
    cob_runtime_error (_("can't open translation table '%s'"), col_name);   // col_name passed verbatim, no length guard
    return -1;
}

cob_runtime_error() delegates formatting to cob_setup_runtime_error_str(), which writes the formatted message into the fixed global buffer with unbounded vsprintf:

// libcob/common.c:351
#define COB_ERRBUF_SIZE     1024

// libcob/common.c:433
static char         runtime_err_str[COB_ERRBUF_SIZE] = { 0 };

// libcob/common.c:9044-9059
static void COB_NOINLINE
cob_setup_runtime_error_str (const char *fmt, va_list ap)
{
    char *p = runtime_err_str;
    /* ... optional "file:line: " prefix via sprintf, advancing p ... */
    vsprintf (p, fmt, ap);          // line 9058 — unbounded; should be vsnprintf(p, runtime_err_str + COB_ERRBUF_SIZE - p, fmt, ap)
}

The PoC reaches the bug by initializing the runtime, entering a module, and calling cob_load_collation() with a ~1099-byte non-path name. Because the name does not begin with . or /, cconv.c takes the config-dir branch and constructs "<COB_CONFIG_DIR>/<name>.ttbl"; that file does not exist, fopen() returns NULL, and cob_runtime_error("can't open translation table '%s'", col_name) formats the full name into the 1024-byte buffer, writing 1131 bytes — a global-buffer-overflow.

This is a library bug, not API misuse:

  • cob_load_collation is declared in libcob/common.h as cob_load_collation (const char *, cob_u8_t *, cob_u8_t *) with the header comment: "Load a file ... Return 0 on success or -1 on error (file unknown or containing invalid data)." No length restriction is documented for col_name; the two output table pointers are explicitly allowed to be NULL.
  • A caller passing a long, non-existent table name to receive the documented -1 return is within contract. The library owns the error-reporting path and the fixed-size buffer; the overflow happens entirely inside cob_setup_runtime_error_str() before control returns to the caller.
  • Note the asymmetry: cconv.c guards the filename against COB_FILE_MAX (4095) — far larger than COB_ERRBUF_SIZE (1024) — but never truncates col_name before handing it to cob_runtime_error(), leaving a wide window (names between ~1010 and 4090 bytes) where the filename fits but the error message overflows.

PoC Code

// poc.cpp
#include <cstdint>
#include <cstring>

extern "C" {
#include "libcob/common.h"
}

extern "C" int cob_load_collation(const char *col_name,
                                  cob_u8_t *ebcdic_to_ascii,
                                  cob_u8_t *ascii_to_ebcdic);

int main(void) {
    /* Initialize the GnuCOBOL runtime and enter a module so the runtime-error
       machinery has a valid module context. */
    char *argv[] = { (char *)"poc", NULL };
    cob_init(1, argv);

    cob_module *module = NULL;
    cob_global *cobglobptr = NULL;
    cob_module_enter(&module, &cobglobptr, 0);
    /* cob_runtime_error inspects the current module's procedure params; give it
       a valid zeroed array so the error path is reached cleanly. */
    static cob_field *proc_params[4] = { NULL, NULL, NULL, NULL };
    module->cob_procedure_params = proc_params;

    /* A collation name well over the 1024-byte runtime_err_str limit. It must
       NOT start with '.' or '/' so cconv.c takes the config-dir branch
       (snprintf into filename[COB_FILE_MAX]) and the .ttbl file does not exist,
       forcing the fopen()==NULL error path. */
    char name[1100];
    memset(name, 'A', sizeof(name) - 1);
    name[sizeof(name) - 1] = '\0';

    unsigned char coll_table[256];
    unsigned char reverse_table[256];
    memset(coll_table, 0, sizeof(coll_table));
    memset(reverse_table, 0, sizeof(reverse_table));

    /* fopen() fails -> cob_runtime_error("can't open translation table '%s'",
       col_name) -> vsprintf overflows runtime_err_str[1024]. */
    cob_load_collation(name, coll_table, reverse_table);

    return 0;
}

Stack Trace

Full sanitizer output from the minimized main() PoC:

=================================================================
==561146==ERROR: AddressSanitizer: global-buffer-overflow on address 0x64024f2f09e0 at pc 0x64024e48ca2f bp 0x7fff63025e90 sp 0x7fff63025640
WRITE of size 1131 at 0x64024f2f09e0 thread T0
    #0 0x64024e48ca2e in vsprintf (/tmp/gnucobol_poc/poc+0x1e7a2e) (BuildId: 25ce9e5fd15f829dc48a4ceccfd1427f7eb6e43b)
    #1 0x64024e5a87b7 in cob_setup_runtime_error_str /tmp/gnucobol_latest/libcob/common.c:9058:2
    #2 0x64024e5640d7 in cob_runtime_error /tmp/gnucobol_latest/libcob/common.c:9071:2
    #3 0x64024e81b0ef in cob_load_collation /tmp/gnucobol_latest/libcob/cconv.c:170:3
    #4 0x64024e54e41e in main /tmp/gnucobol_poc/poc.cpp:70:5
    #5 0x7ca3c42a51c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #6 0x7ca3c42a528a in __libc_start_main csu/../csu/libc-start.c:360:3
    #7 0x64024e463264 in _start (/tmp/gnucobol_poc/poc+0x1be264) (BuildId: 25ce9e5fd15f829dc48a4ceccfd1427f7eb6e43b)

0x64024f2f09e0 is located 0 bytes after global variable 'runtime_err_str' defined in '/tmp/gnucobol_latest/libcob/common.c:433' (0x64024f2f05e0) of size 1024
SUMMARY: AddressSanitizer: global-buffer-overflow (/tmp/gnucobol_poc/poc+0x1e7a2e) (BuildId: 25ce9e5fd15f829dc48a4ceccfd1427f7eb6e43b) in vsprintf
Shadow bytes around the buggy address:
  0x64024f2f0700: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x64024f2f0780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x64024f2f0800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x64024f2f0880: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x64024f2f0900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x64024f2f0980: 00 00 00 00 00 00 00 00 00 00 00 00[f9]f9 f9 f9
  0x64024f2f0a00: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9
  0x64024f2f0a80: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 00 f9 f9 f9
  0x64024f2f0b00: 00 f9 f9 f9 04 f9 f9 f9 04 f9 f9 f9 00 00 00 00
  0x64024f2f0b80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 f9
  0x64024f2f0c00: f9 f9 f9 f9 00 f9 f9 f9 04 f9 f9 f9 00 f9 f9 f9
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==561146==ABORTING

Reproduction Step

  1. Checkout GnuCOBOL at the version above:
git clone https://github.com/OCamlPro/gnucobol.git
cd gnucobol
git checkout a672a26b52b594bd0ebfdcfe0200613c572018d5   # origin/gnucobol-3.x tip
  1. Build the library with AddressSanitizer + UBSan (static):
# build deps (Ubuntu/Debian):
sudo apt-get install -y clang clang++ autopoint gettext help2man bison flex \
    libgmp-dev libxml2-dev libjson-c-dev libncurses-dev libdb-dev

export CC=clang CXX=clang++
export CFLAGS="-fsanitize=address,undefined -fno-sanitize=function \
  -fsanitize-address-use-after-scope -g -O0 -fno-omit-frame-pointer -fPIC"
export CXXFLAGS="$CFLAGS" LDFLAGS="$CXXFLAGS"
export WORK=$PWD/_san            # install prefix

autoreconf --force --install -I m4
mkdir _build && cd _build
../configure --prefix="$WORK" --enable-static --disable-shared \
    --with-math=gmp --with-json=json-c --enable-debug --disable-hardening \
    CC="$CC" CXX="$CXX" CFLAGS="$CFLAGS" CXXFLAGS="$CXXFLAGS"
make -j"$(nproc)"
make install
  1. Build the PoC against that sanitizer build (standalone main(), no -fsanitize=fuzzer):
cd /path/to/poc_dir   # contains poc.cpp
clang++ -g -O0 -fno-omit-frame-pointer -ftrivial-auto-var-init=zero \
    -fsanitize=address,undefined -fno-sanitize=function \
    -fsanitize-address-use-after-scope \
    -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION \
    -I"$WORK/include" poc.cpp -o poc \
    -Wl,--start-group "$WORK"/lib/lib*.a -Wl,--end-group \
    -lgmp -lxml2 -ljson-c -lpanelw -lncursesw -ltinfo -ldb-5.3
  1. Run the PoC and observe the crash:
./poc
# => AddressSanitizer: global-buffer-overflow ... WRITE of size 1131
#    #1 cob_setup_runtime_error_str libcob/common.c:9058
#    #2 cob_runtime_error           libcob/common.c:9071
#    #3 cob_load_collation          libcob/cconv.c:170
#    #4 main                        poc.cpp:70

The crash reproduces on the latest origin/gnucobol-3.x tip (a672a26b); the vulnerable lines are unchanged from the snapshot under test.

Submission Statement

This report was produced by FuzzAnything's AI-assisted library fuzzer and manually verified by a team member. We reviewed the PoC against the upstream API documentation — call order, parameters, and memory ownership — and found no API misuse.

Signed-off-by: FuzzAnything fuzzanything@gmail.com

Related

Bugs: #1245

Discussion

  • Simon Sobisch

    Simon Sobisch - 4 days ago
    • labels: --> SIGSEGV, libcob, good-first-issue
    • summary: Signed-integer overflow in cob_display_get_int reached via FUNCTION FORMATTED-DATE --> string overflow in runtime_err_str via cob_load_collation
    • status: open --> accepted
    • Group: unclassified --> GC 3.x
    • Priority: 5 - default --> 7
     
  • Simon Sobisch

    Simon Sobisch - 4 days ago

    The title was totally wrong. @psy99 There should be a proper bug report for "Signed-integer overflow in cob_display_get_int reached via FUNCTION FORMATTED-DATE"... but the string overflow presented here (mentioned already in [bugs:#1245] by @imiab) is indeed an issue.

    I've lowered the priority as this needs either a manual library call to libcob with a very bad parameter or the main use (call from cobc) with the same.

    Easy reproducer:
    cobc -febcdic-table=$(printf '%2000s' ' ' | tr ' ' 'X') -

    To Do:

    • cob_runtime_error: use of vsnprintf, check the return code and on error output the message, then call cob_runtime_warning, ideally with a report note (as that is a common function that can be executed during "normal" runs: we don't want an abort there) -> indirectly "solves" (the SIGSEGV part of) [bugs:#1245]
    • write a small C test program that calls cob_runtimer_error directly with 3000 bytes, included in the testsuite under used_binaries.at
    • cob_load_collation: before calling cob_runtime_warning: check col_name's size and - as we don't expect that to be a problem that the user normally will see - trim the filename down if too big
    • add the sample call to cobc above to the testsuite (also used_binaries.at), verifying the expected outcome
     

    Related

    Bugs: #1245


Log in to post a comment.