Summary
Two CAM code paths read 64-bit NetCDF dimension lengths into default (32-bit) Fortran integers, multiply them to compute an allocation size, and then fill the resulting array with the true (64-bit) element count. When the dimension product exceeds 2**31, the 32-bit product wraps to a small — or negative, and hence clamped to a zero/short — value. The allocation is therefore undersized while the subsequent write loop runs to the true length, producing a heap out-of-bounds write (CWE-190 → CWE-787). Both sites are reachable from an attacker-supplied / crafted NetCDF input file, so this is a remotely-triggerable denial-of-service / heap-integrity violation.
Neither site validates the dimension factors against a compiled-in maximum before multiplying.
Affected code
Site 1 — src/control/sat_hist.F90 @ cam_development
The profs NetCDF dimension of the satellite-track file is read into the default-integer n_profiles (declared L62), which bounds ncols:
! L62
integer :: n_profiles
...
! L216-217 (subroutine sat_hist_init, reading sathist_track_infile)
ierr = pio_inq_dimid(infile,'profs',dimid)
ierr = pio_inq_dimlen(infile, dimid, n_profiles)
ncols (default integer, L425) is derived from that count (ncols = end_ndx-beg_ndx+1, L713) and multiplied by the namelist value sathist_nclosest (default integer, L45) to size nine arrays:
! L425
integer :: ncols, nocols
...
! L443
call read_next_position( ncols )
...
! L451-461
nocols = ncols * sathist_nclosest
allocate( col_ndxs(nocols) )
allocate( chk_ndxs(nocols) )
allocate( fdyn_ndxs(nocols) )
allocate( ldyn_ndxs(nocols) )
allocate( phs_owners(nocols) )
allocate( dyn_owners(nocols) )
allocate( mlats(nocols) )
allocate( mlons(nocols) )
allocate( phs_dists(nocols) )
The same wrapping product recurs in dump_columns (L762-763) and in write_output (outdata), where the fill loop indexes to the true count:
! L986-990
allocate( outdata(ncols * sathist_nclosest) )
do i = 1, ncols
outdata(((i-1)*sathist_nclosest)+1 : (i*sathist_nclosest)) = data(i)
enddo
sathist_nclosest is a satellite_options_nl namelist variable (declared L45, in namelist L128-129, default 1 at L137) and is not range-checked. Abort routine in this file: use cam_abortutils, only: endrun (L14).
Site 2 — tools/mkatmsrffile/mkatmsrffile.F90 @ cam_development
The grid_size dimension of the atmosphere NetCDF file (atmFileName) is read into the default-integer nxg, divided to nx, and used to size gindex; atmnx (default integer, L52) — the same grid size returned via openfile_and_initdecomp (L97) — sizes dof2:
! L52
integer :: srfnx, atmnx, srfnxg, atmnxg, dimid, nlat, nlon, i, j, clen, index, dim1, dim2
...
! L294-299
allocate(dof2(atmnx*12))
do j=1,12
do i=1,atmnx
dof2(i+(j-1)*atmnx) = dof(i)+(j-1)*atmnxg
end do
end do
...
! L326 / L346 (get_grid_index: nx, nxg are default integer)
integer, intent(out) :: nx, nxg
...
! L356-368
ierr = pio_inq_dimid(File, 'grid_size', dimid)
ierr = pio_inq_dimlen(File, dimid, nxg)
nx = nxg/npes
...
allocate(gindex(nx+add1))
do i=1,nx+add1
gindex(i)=i+iam*nx+start_offset
end do
atmnx*12 and nxg are default 32-bit throughout with no bounds check. This file currently has no abort routine imported (it already uses shr_kind_mod from CIME share code, so shr_sys_abort is available).
Root cause
pio_inq_dimlen returns a 64-bit on-disk dimension length, but the receiving variables (n_profiles, nxg, and the derived ncols, atmnx) are default integer (32-bit). The allocation size is formed as a 32-bit product:
nocols = ncols * sathist_nclosest
atmnx*12, nx+add1
Once the true product exceeds the signed 32-bit range (2**31), the multiply wraps modulo 2**32, yielding a small positive, or a negative (→ effectively zero-length) allocation. The fill loops, however, iterate over the true element count, so every element past the wrapped size is written out of bounds on the heap.
Trigger / attack vector
- Site 1: A crafted satellite-track NetCDF referenced by
sathist_track_infile (satellite_options_nl) with a large profs dimension, combined with a large sathist_nclosest, such that ncols * sathist_nclosest overflows 32-bit. sathist_nclosest is operator-controlled and unbounded; ncols/profs come directly from the input file.
- Site 2: A crafted atmosphere NetCDF (
atmFileName, the mkatmsrffile offline tool) with a grid_size dimension whose value (or grid_size*12) overflows 32-bit.
In both cases the malicious value lives entirely in a NetCDF input file, so the crash is reachable without any code change on the target.
Impact
- Classification: CWE-190 (integer overflow) → CWE-787 (out-of-bounds write).
- Severity effect: remotely-triggerable denial of service / heap-integrity violation from a crafted input file.
- Not RCE: the overflow deficit is structurally unbounded (≥
2**31 elements), so the contiguous fill loop runs off the end of mapped memory and the process crashes during the loop, before any overwritten heap metadata or pointer could be dereferenced — there is no controlled-write primitive an attacker could steer toward code execution.
Reproduction
Construct a NetCDF file whose relevant dimensions multiply to just over 2**32:
- Site 1:
profs (→ ncols) × sathist_nclosest slightly above 2**32 (e.g. ncols ≈ 2.2e9, or a modest ncols with a large namelist sathist_nclosest).
- Site 2:
grid_size such that grid_size*12 slightly exceeds 2**32.
The 32-bit product wraps to a small value, allocate under-sizes the buffer, and the fill loop writes past it. A minimal harness (ifx -fsanitize=address) that mirrors the sat_hist.F90 structure exactly —
nocols = ncols * sathist_nclosest ! == L451, 32-bit product
allocate( outdata(nocols) ) ! == L986
do i = 1, ncols
outdata( (i-1)*sathist_nclosest+1 : i*sathist_nclosest ) = i ! == L988-990
end do
— reproduces it deterministically with ncols=65536, sathist_nclosest=65537 (true product 4,295,032,832 → wraps to 65536):
[sat_hist] nocols(int32)=65536
[sat_hist] allocated outdata(65536)
==ERROR: AddressSanitizer: heap-buffer-overflow WRITE of size 4 at MAIN ... :19
0x... is located 0 bytes after 262184-byte region
SUMMARY: AddressSanitizer: heap-buffer-overflow ... in MAIN
i.e. the first write past the wrapped allocation is flagged at the fill loop. (Build: ifx -O0 -g -fsanitize=address, glibc 2.38.)
Suggested fix
Preferred: bound each dimension factor against a compiled-in maximum immediately after it is read, aborting via the component's standard routine so the product can never overflow. (Equivalently, compute the product in integer(8) and validate before allocate.)
src/control/sat_hist.F90
! module-scope parameter (choose a value well above any realistic size,
! but far below the 32-bit limit so the product cannot wrap)
integer, parameter :: max_profs = 100000000 ! 1e8 satellite profiles
integer, parameter :: max_nclosest = 100000
! ---- immediately after L217, having read the 'profs' dimension ----
ierr = pio_inq_dimlen(infile, dimid, n_profiles)
if (n_profiles < 0 .or. n_profiles > max_profs) then
call endrun('sat_hist: profs dimension in '//trim(locfn)// &
' is out of range (possible 32-bit integer overflow)')
end if
! ---- after the namelist read / mpibcast of sathist_nclosest ----
if (sathist_nclosest < 1 .or. sathist_nclosest > max_nclosest) then
call endrun('sat_hist: sathist_nclosest out of range')
end if
! ---- belt-and-suspenders at the product site (replaces L451) ----
if (int(ncols,8) * int(sathist_nclosest,8) > int(huge(nocols),8)) then
call endrun('sat_hist: ncols*sathist_nclosest overflows 32-bit integer')
end if
nocols = ncols * sathist_nclosest
tools/mkatmsrffile/mkatmsrffile.F90
use shr_sys_mod, only : shr_sys_abort ! add to the use list
integer, parameter :: max_grid_size = 100000000
! ---- immediately after L357, having read 'grid_size' ----
ierr = pio_inq_dimlen(File, dimid, nxg)
if (nxg < 0 .or. nxg > max_grid_size) then
call shr_sys_abort('mkatmsrffile: grid_size out of range in '//trim(filename)// &
' (possible 32-bit integer overflow)')
end if
The int(...,8) guard (or a compiled-in max_* bound) is sufficient at each site; the two together are belt-and-suspenders.
Severity
Medium — remotely-triggerable crash / heap corruption from a crafted NetCDF input; no privilege escalation or code execution.
Summary
Two CAM code paths read 64-bit NetCDF dimension lengths into default (32-bit) Fortran integers, multiply them to compute an allocation size, and then fill the resulting array with the true (64-bit) element count. When the dimension product exceeds
2**31, the 32-bit product wraps to a small — or negative, and hence clamped to a zero/short — value. The allocation is therefore undersized while the subsequent write loop runs to the true length, producing a heap out-of-bounds write (CWE-190 → CWE-787). Both sites are reachable from an attacker-supplied / crafted NetCDF input file, so this is a remotely-triggerable denial-of-service / heap-integrity violation.Neither site validates the dimension factors against a compiled-in maximum before multiplying.
Affected code
Site 1 —
src/control/sat_hist.F90@cam_developmentThe
profsNetCDF dimension of the satellite-track file is read into the default-integern_profiles(declared L62), which boundsncols:ncols(default integer, L425) is derived from that count (ncols = end_ndx-beg_ndx+1, L713) and multiplied by the namelist valuesathist_nclosest(default integer, L45) to size nine arrays:! L425 integer :: ncols, nocols ... ! L443 call read_next_position( ncols ) ... ! L451-461 nocols = ncols * sathist_nclosest allocate( col_ndxs(nocols) ) allocate( chk_ndxs(nocols) ) allocate( fdyn_ndxs(nocols) ) allocate( ldyn_ndxs(nocols) ) allocate( phs_owners(nocols) ) allocate( dyn_owners(nocols) ) allocate( mlats(nocols) ) allocate( mlons(nocols) ) allocate( phs_dists(nocols) )The same wrapping product recurs in
dump_columns(L762-763) and inwrite_output(outdata), where the fill loop indexes to the true count:sathist_nclosestis asatellite_options_nlnamelist variable (declared L45, in namelist L128-129, default1at L137) and is not range-checked. Abort routine in this file:use cam_abortutils, only: endrun(L14).Site 2 —
tools/mkatmsrffile/mkatmsrffile.F90@cam_developmentThe
grid_sizedimension of the atmosphere NetCDF file (atmFileName) is read into the default-integernxg, divided tonx, and used to sizegindex;atmnx(default integer, L52) — the same grid size returned viaopenfile_and_initdecomp(L97) — sizesdof2:atmnx*12andnxgare default 32-bit throughout with no bounds check. This file currently has no abort routine imported (it alreadyusesshr_kind_modfrom CIME share code, soshr_sys_abortis available).Root cause
pio_inq_dimlenreturns a 64-bit on-disk dimension length, but the receiving variables (n_profiles,nxg, and the derivedncols,atmnx) are defaultinteger(32-bit). The allocation size is formed as a 32-bit product:nocols = ncols * sathist_nclosestatmnx*12,nx+add1Once the true product exceeds the signed 32-bit range (
2**31), the multiply wraps modulo2**32, yielding a small positive, or a negative (→ effectively zero-length) allocation. The fill loops, however, iterate over the true element count, so every element past the wrapped size is written out of bounds on the heap.Trigger / attack vector
sathist_track_infile(satellite_options_nl) with a largeprofsdimension, combined with a largesathist_nclosest, such thatncols * sathist_nclosestoverflows 32-bit.sathist_nclosestis operator-controlled and unbounded;ncols/profscome directly from the input file.atmFileName, themkatmsrffileoffline tool) with agrid_sizedimension whose value (orgrid_size*12) overflows 32-bit.In both cases the malicious value lives entirely in a NetCDF input file, so the crash is reachable without any code change on the target.
Impact
2**31elements), so the contiguous fill loop runs off the end of mapped memory and the process crashes during the loop, before any overwritten heap metadata or pointer could be dereferenced — there is no controlled-write primitive an attacker could steer toward code execution.Reproduction
Construct a NetCDF file whose relevant dimensions multiply to just over
2**32:profs(→ncols) ×sathist_nclosestslightly above2**32(e.g.ncols ≈ 2.2e9, or a modestncolswith a large namelistsathist_nclosest).grid_sizesuch thatgrid_size*12slightly exceeds2**32.The 32-bit product wraps to a small value,
allocateunder-sizes the buffer, and the fill loop writes past it. A minimal harness (ifx -fsanitize=address) that mirrors thesat_hist.F90structure exactly —— reproduces it deterministically with
ncols=65536, sathist_nclosest=65537(true product4,295,032,832→ wraps to65536):i.e. the first write past the wrapped allocation is flagged at the fill loop. (Build:
ifx -O0 -g -fsanitize=address, glibc 2.38.)Suggested fix
Preferred: bound each dimension factor against a compiled-in maximum immediately after it is read, aborting via the component's standard routine so the product can never overflow. (Equivalently, compute the product in
integer(8)and validate beforeallocate.)src/control/sat_hist.F90tools/mkatmsrffile/mkatmsrffile.F90The
int(...,8)guard (or a compiled-inmax_*bound) is sufficient at each site; the two together are belt-and-suspenders.Severity
Medium — remotely-triggerable crash / heap corruption from a crafted NetCDF input; no privilege escalation or code execution.