Sanitizer (ASan + UBSan) run of gunittest and pytest suites: 20 findings (2026-10-11)
Maintainers usually reply within 1 day
@Jayant-kernel is already working on this.
Since Oct 19, 2026.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Domain
- backend, testing-qa
Research direction
Start with the verified findings in the issue, especially the named locations in include/grass/iostream/ami_sort.h, include/grass/iostream/ami_stream.h, raster/r.reclass/reclass.c, raster/r.stats/stats.c, and raster/r.univar/r.univar_main.c. Read each sanitizer report and its verification notes in the attached archive to understand the reproduction and affected test. Done means confirmed findings have focused fixes and regression coverage; this spans multiple subsystems and needs careful review.
Written by the indexing model from the issue text.
Description
Summary
One-off clang ASan + UBSan pass over the GRASS gunittest (testsuite/) and pytest (tests/) tests. All entries below are reported by the sanitizers; "likely cause" notes are hypotheses from reading the code at the reported line, not confirmed bugs.
AI filtered out most of the noise already. Categorized as HIGH, MEDIUM or LOW.
Attached here are some extra files too from the run, like the raw logs of all issues: sanitizer-run-4.tar.gz
- GRASS commit:
8f7506115d6125bf18f9edd87a39d4b7cbee59b7 - Compiler: Ubuntu clang 18.1.3; OS: Ubuntu 24.04.5 LTS (x86_64), CMake + Ninja build,
-DWITH_PDAL=OFF -DWITH_GUI=OFF -DWITH_X11=OFF -DWITH_OPENGL=OFF -DWITH_DOCS=OFF
Build flags, run configuration, dedup method
-
Build flags:
CFLAGS=CXXFLAGS="-g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined -fsanitize-recover=address,undefined -fno-sanitize=function",LDFLAGS="-fsanitize=address,undefined -shared-libasan -Wl,-rpath,<clang rt dir>" -
Run: all 318
testsuite/test*.pyfiles (in-tree copy), 4 in parallel, each in a temporary mapset of thenc_spm_08_grass7(basic) sample dataset, 600 s timeout per file; wall time ~31 min. 270 files exited 0, 46 exited 1 (see Not covered), 2 aborted (see Filtered out). -
pytest run: all 172
tests/**/*_test.pyandtest_*.pyfiles (one pytest process per file, 4 in parallel, pytest 9.1.1 in a venv because the repo requires >= 8.2,--timeout=600),grassfrom the build tree onPATH; wall time ~13 min. 164 files passed, 6 had failing/erroring tests (not sanitizer related: missingosgeo._gdalfor the venv/system GDAL python bindings,g.extensionneeding a network/installed GRASS,r.packrunning as root so the non-writable-dir test did not fail,man/g.search.modulesneeding the GUI tree), 2 files exited 5 (no tests collected). -
Counts: 4601 raw reports -> 4526 in GRASS code -> 20 unique findings (6 HIGH, 6 MEDIUM, 8 LOW). 75 reports filtered as third-party only.
-
Verification: after triage, each finding was re-checked by a separate pass that read the code and reproduced it with a minimal command (scripts and notes in
verify/). Each checklist item carries a[verified: ...]tag. One additional finding (H06,r.statsINT_MIN negation) was found during this step and was not seen in the test runs, so it is not counted in the raw-report numbers.
Counts cover both runs (gunittest 4513 raw, pytest 88 raw). Findings seen in both are merged. Dedup key: sanitizer + error type + top in-GRASS frame (UBSan) or top in-GRASS frame + allocation site (ASan). The "top 3 frames" key was relaxed because template instantiations in r.terraflow and varying callers of library functions (G_ls2) split one location into many keys; stacks below show the first example.
Findings
| ID | Type | Location | Tests | Hits |
|---|---|---|---|---|
| H01 | alloc-dealloc-mismatch |
include/grass/iostream/ami_sort.h:133 |
1 | 18 |
| H02 | alloc-dealloc-mismatch |
include/grass/iostream/ami_stream.h:506 |
1 | 4071 |
| H03 | alloc-dealloc-mismatch |
include/grass/iostream/ami_stream.h:506 |
1 | 67 |
| H04 | alloc-dealloc-mismatch |
include/grass/iostream/replacementHeap.h:154 |
1 | 2 |
| H05 | heap-buffer-overflow |
raster/r.reclass/reclass.c:58 |
1 | 1 |
| H06 | INT_MIN negation (OOB read) |
raster/r.stats/stats.c:137 |
0 | 0 |
| M01 | signed-integer-overflow |
lib/raster3d/long.c:27 |
28 | 183 |
| M02 | float-cast-overflow |
raster/r.sim/simlib/hydro.c:112 |
1 | 1 |
| M03 | signed-integer-overflow |
raster/r.stats/stats.c:134 |
2 | 3 |
| M04 | signed-integer-overflow |
raster/r.stats/stats.c:134 |
2 | 3 |
| M05 | signed-integer-overflow |
raster/r.texture/h_measure.c:373 |
2 | 2 |
| M06 | signed-integer-overflow |
raster/r.univar/r.univar_main.c:516 |
1 | 3 |
| L01 | array-bounds |
lib/gis/aprintf.c:198 |
1 | 1 |
| L02 | nonnull-attribute |
lib/gis/ls.c:116 |
29 | 115 |
| L03 | nonnull-attribute |
lib/gis/plot.c:455 |
1 | 1 |
| L04 | pointer-overflow (null + offset) |
raster/r.fill.stats/main.c:595 |
1 | 4 |
| L05 | pointer-overflow (null + offset) |
raster/r.sun/main.c:1955 |
1 | 5 |
| L06 | pointer-overflow (null + offset) |
raster/r.sun/main.c:1972 |
1 | 8 |
| L07 | nonnull-attribute |
vector/v.extract/extract.c:35 |
5 | 19 |
| L08 | nonnull-attribute |
vector/v.extract/extract.c:285 |
5 | 19 |
Details (stack, message, repro, raw log) for each item are in the collapsed blocks under Details below.
HIGH
- H01
alloc-dealloc-mismatchininclude/grass/iostream/ami_sort.h:133(AMI_sort()):nameis returned byAMI_STREAM::name()allocated withnew char[](ami_stream.h:429) but freed with scalardelete name. [verified: confirmed] - H02
alloc-dealloc-mismatchininclude/grass/iostream/ami_stream.h:506(AMI_STREAM::~AMI_STREAM()): The stream bufferbufis allocated withnew char[](ami_stream.h lines 236/281) but released with scalardelete bufin the destructor. [verified: confirmed] - H03
alloc-dealloc-mismatchininclude/grass/iostream/ami_stream.h:506(AMI_STREAM::~AMI_STREAM()): The stream bufferbufis allocated withnew char[](ami_stream.h lines 236/281) but released with scalardelete bufin the destructor. [verified: confirmed] - H04
alloc-dealloc-mismatchininclude/grass/iostream/replacementHeap.h:154(ReplacementHeap::ReplacementHeap()): Same as ami_sort.h:133:namefromAMI_STREAM::name()(new char[]) freed with scalardelete. [verified: confirmed] - H05
heap-buffer-overflowinraster/r.reclass/reclass.c:58(compose()): Incompose(), if no old category maps through the intermediate table,new->min = new->max = 0is set but the second loop still indexesold->table[i - old->min]with i = 0; forold->min> 0 this reads before the table (ASan: 4 bytes before the region, allocated inget_reclass_table). [verified: confirmed] - H06
INT_MIN negation (OOB read)inraster/r.stats/stats.c:137(update_cell_stats()):-hashof INT_MIN stays negative, so the hash table is indexed with a negative value. [verified: confirmed, real memory bug (found while verifying M04/M05; not seen in the test runs)]
MEDIUM
- M01
signed-integer-overflowinlib/raster3d/long.c:27(Rast3d_long_encode()):d *= 256runs once more than needed on the last byte iteration, overflowinglong(256^8); the value is not used afterwards. [verified: benign] - M02
float-cast-overflowinraster/r.sim/simlib/hydro.c:112(main_loop()):gen = rwalk * si / sisumis NaN (likelysisum== 0), then(int)gencasts NaN to int. [verified: benign on x86-64] - M03
signed-integer-overflowinraster/r.stats/stats.c:134(update_cell_stats()): The hashhash * HASHMOD + valueintentionally wraps around for large category values; overflow of signed int is UB, though the result is only used as a bucket index. [verified: hash overflow benign; see H06] - M04
signed-integer-overflowinraster/r.stats/stats.c:134(update_cell_stats()): The hashhash * HASHMOD + valueintentionally wraps around for large category values; overflow of signed int is UB, though the result is only used as a bucket index. [verified: hash overflow benign; see H06] - M05
signed-integer-overflowinraster/r.texture/h_measure.c:373(f10_dvar()):tmp * tmpis computed inintwheretmpis the squared tone range; for wide ranges (47961^2) it overflows before the float division. [verified: output changes slightly] - M06
signed-integer-overflowinraster/r.univar/r.univar_main.c:516(process_raster()):val * valis computed inintbefore being added to the floating-pointsumsq; values above ~46340 overflow. [verified: WRONG OUTPUT]
LOW
- L01
array-boundsinlib/gis/aprintf.c:198(oaprintf()):char *c = CONVS - 1;forms a pointer one before the start of the string literal, then pre-increments it; the sanitizer flags the index -1. [verified: formal UB only] - L02
nonnull-attributeinlib/gis/ls.c:116(G_ls2()):qsort(dir_listing, n, ...)is called withdir_listing == NULLwhen the directory has no (visible) entries (n = 0). [verified: formal UB only] - L03
nonnull-attributeinlib/gis/plot.c:455(G_plot_polygon()):qsort(st->P, st->np, ...)with a NULLst->Pwhen the polygon has no edge points (np = 0). [verified: formal UB only] - L04
pointer-overflow (null + offset)inraster/r.fill.stats/main.c:595(interpolate_row()):err_output++advances a pointer that is NULL when the error map is not requested (write_errfalse). [verified: formal UB only] - L05
pointer-overflow (null + offset)inraster/r.sun/main.c:1955(calculate()): Same pattern as themain.c:1972finding:horizonarrayis NULL with a non-zero offset (call tolumcline2). [verified: formal UB only] - L06
pointer-overflow (null + offset)inraster/r.sun/main.c:1972(calculate()):horizonarray + shadowoffsetis evaluated whilehorizonarrayis NULL (no horizon input) andshadowoffsetis non-zero. [verified: formal UB only] - L07
nonnull-attributeinvector/v.extract/extract.c:35(in_list()):bsearch(&cat, cats_array, ncats_array, ...)with NULLcats_arrayand zero count. [verified: formal UB only] - L08
nonnull-attributeinvector/v.extract/extract.c:285(extract_line()):qsort(cats_array, ncats_array, ...)withcats_arrayNULL when no categories were collected. [verified: formal UB only]
Details
H01 alloc-dealloc-mismatch in include/grass/iostream/ami_sort.h:133
- Sanitizer: ASan
- Triggered by:
raster/r.terraflow/testsuite/test_r_terraflow.py, hits: 18 - Message:
AddressSanitizer: alloc-dealloc-mismatch (operator new [] vs operator delete) on 0x506000003e00 - Allocated at:
include/grass/iostream/ami_stream.h:429 - Likely cause (hypothesis):
nameis returned byAMI_STREAM::name()allocated withnew char[](ami_stream.h:429) but freed with scalardelete name. Hint:delete[] name. - Repro (gunittest):
cd raster/r.terraflow/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_r_terraflow.py(gunittest environment below) - Raw log:
raw-logs/raster_r.terraflow_testsuite_test_r_terraflow.py/ubsan.7851 - Verification (verified: confirmed): Same family (name strings from
AMI_STREAM::name()). - Minimal repro:
verify/terraflow/
Trimmed stack:
include/grass/iostream/ami_sort.h:133 AMI_err AMI_sort<plateauType, ijCmpPlateauType>
raster/r.terraflow/sortutils.h:42 void sort<plateauType, ijCmpPlateauType>(AMI_STREAM<plateauT
raster/r.terraflow/plateau.cpp:335 detectPlateaus::removeDuplicates
raster/r.terraflow/plateau.cpp:434 findPlateaus
raster/r.terraflow/fill.cpp:195 computeFlowDirections
raster/r.terraflow/main.cpp:560 main
H02 alloc-dealloc-mismatch in include/grass/iostream/ami_stream.h:506
- Sanitizer: ASan
- Triggered by:
raster/r.terraflow/testsuite/test_r_terraflow.py, hits: 4071 - Message:
AddressSanitizer: alloc-dealloc-mismatch (operator new [] vs operator delete) on 0x7fb05057d800 - Allocated at:
include/grass/iostream/ami_stream.h:281 - Likely cause (hypothesis): The stream buffer
bufis allocated withnew char[](ami_stream.h lines 236/281) but released with scalardelete bufin the destructor. Hint:delete[] buf. - Repro (gunittest):
cd raster/r.terraflow/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_r_terraflow.py(gunittest environment below) - Raw log:
raw-logs/raster_r.terraflow_testsuite_test_r_terraflow.py/ubsan.7851 - Verification (verified: confirmed): One root-cause family:
new char[]released with scalardelete. See the list of additional sites at the end. - Minimal repro: small
r.terraflowrun, scripts inverify/terraflow/
Trimmed stack:
include/grass/iostream/ami_stream.h:506 AMI_STREAM<float>::~AMI_STREAM
raster/r.terraflow/3scan.h:192 void scan3<float, float, detectEdgeNodata>(AMI_STREAM<float>
raster/r.terraflow/nodata.cpp:250 detectEdgeNodata::generateNodata
raster/r.terraflow/nodata.cpp:314 classifyNodata
raster/r.terraflow/fill.cpp:179 computeFlowDirections
raster/r.terraflow/main.cpp:560 main
H03 alloc-dealloc-mismatch in include/grass/iostream/ami_stream.h:506
- Sanitizer: ASan
- Triggered by:
raster/r.terraflow/testsuite/test_r_terraflow.py, hits: 67 - Message:
AddressSanitizer: alloc-dealloc-mismatch (operator new [] vs operator delete) on 0x7fb050728800 - Allocated at:
include/grass/iostream/ami_stream.h:236 - Likely cause (hypothesis): The stream buffer
bufis allocated withnew char[](ami_stream.h lines 236/281) but released with scalardelete bufin the destructor. Hint:delete[] buf. - Repro (gunittest):
cd raster/r.terraflow/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_r_terraflow.py(gunittest environment below) - Raw log:
raw-logs/raster_r.terraflow_testsuite_test_r_terraflow.py/ubsan.7851 - Verification (verified: confirmed): One root-cause family:
new char[]released with scalardelete. See the list of additional sites at the end. - Minimal repro: small
r.terraflowrun, scripts inverify/terraflow/
Trimmed stack:
include/grass/iostream/ami_stream.h:506 AMI_STREAM<nodataType>::~AMI_STREAM
raster/r.terraflow/nodata.cpp:266 detectEdgeNodata::relabelNodata
raster/r.terraflow/nodata.cpp:331 classifyNodata
raster/r.terraflow/fill.cpp:179 computeFlowDirections
raster/r.terraflow/main.cpp:560 main
H04 alloc-dealloc-mismatch in include/grass/iostream/replacementHeap.h:154
- Sanitizer: ASan
- Triggered by:
raster/r.terraflow/testsuite/test_r_terraflow.py, hits: 2 - Message:
AddressSanitizer: alloc-dealloc-mismatch (operator new [] vs operator delete) on 0x506000003ce0 - Allocated at:
include/grass/iostream/ami_stream.h:429 - Likely cause (hypothesis): Same as ami_sort.h:133:
namefromAMI_STREAM::name()(new char[]) freed with scalardelete. Hint:delete[] name. - Repro (gunittest):
cd raster/r.terraflow/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_r_terraflow.py(gunittest environment below) - Raw log:
raw-logs/raster_r.terraflow_testsuite_test_r_terraflow.py/ubsan.7851 - Verification (verified: confirmed): Only triggers with
memory=1on the full-resolution elevation raster. - Minimal repro:
verify/terraflow/
Trimmed stack:
include/grass/iostream/replacementHeap.h:154 ReplacementHeap<sweepItemBaseType<int>, PrioCmpSweepItem>::R
include/grass/iostream/ami_sort_impl.h:313 AMI_STREAM<sweepItemBaseType<int>>* singleMerge<sweepItemBas
include/grass/iostream/ami_sort_impl.h:356 AMI_STREAM<sweepItemBaseType<int>>* multiMerge<sweepItemBase
include/grass/iostream/ami_sort.h:137 AMI_err AMI_sort<sweepItemBaseType<int>, PrioCmpSweepItem>
raster/r.terraflow/sortutils.h:42 void sort<sweepItemBaseType<int>, PrioCmpSweepItem>(AMI_STRE
raster/r.terraflow/flow.cpp:223 fillstr2sweepstr
H05 heap-buffer-overflow in raster/r.reclass/reclass.c:58
- Sanitizer: ASan
- Triggered by:
lib/raster/tests/lib_raster_reclass_test.py, hits: 1 - Message:
AddressSanitizer: heap-buffer-overflow on address 0x50200000888c at pc 0x55cfd1d9359e bp 0x7fffc1002230 sp 0x7fffc1002228 - Allocated at:
lib/gis/alloc.c:121 - Likely cause (hypothesis): In
compose(), if no old category maps through the intermediate table,new->min = new->max = 0is set but the second loop still indexesold->table[i - old->min]with i = 0; forold->min> 0 this reads before the table (ASan: 4 bytes before the region, allocated inget_reclass_table). Hint: skip the second loop whenfirstis still set. - Repro (pytest):
v/bin/python -m pytest -p no:cacheprovider --timeout=600 lib/raster/tests/lib_raster_reclass_test.pyfrom the repo root (pytest environment below) - Raw log:
raw-logs-pytest/lib_raster_tests_lib_raster_reclass_test.py/ubsan.27994 - Verification (verified: confirmed): Reproduced with a minimal command; hypothesis confirmed exactly. Read only: the write goes to
new->table[0]of a 1-element table (in bounds). The garbage value read can then be used for a second read ofmid->table, so the stored category can be wrong. Trigger test istest_long_names. Without ASan it silently reads adjacent heap bytes and is usually right by luck. - Minimal repro:
r.mapcalc expression="base = 1"; echo "1 = 5" | r.reclass input=base output=rc1 rules=-; echo "1 = 7" | r.reclass input=rc1 output=rc2 rules=-(scriptverify/reclass/repro.sh)
Trimmed stack:
raster/r.reclass/reclass.c:58 compose
raster/r.reclass/reclass.c:200 re_reclass
raster/r.reclass/reclass.c:224 reclass
raster/r.reclass/main.c:141 main
H06 INT_MIN negation (OOB read) in raster/r.stats/stats.c:137
- Sanitizer: UBSan
- Triggered by: found by verification only, hits: 0
- Message:
negation of -2147483648 cannot be represented in type 'int' - Likely cause (hypothesis): unclear
- Verification (verified: confirmed, real memory bug (found while verifying M04/M05; not seen in the test runs)):
if (hash < 0) hash = -hash;leaves INT_MIN unchanged,hash %= HASHSIZEis then negative (-1616), andhashtable[hash]is an out-of-bounds read; ASan reports SEGV atstats.c:142. Needs two maps whose combined hash wraps to INT_MIN (random data: about 1 in 2^32 per cell; crafted input hits it reliably). - Minimal repro:
r.mapcalc a=1000000; r.mapcalc b=2058483648; r.stats -c a,bin a 1x1 region (verify/numeric/m04.sh)
Trimmed stack:
M01 signed-integer-overflow in lib/raster3d/long.c:27
- Sanitizer: UBSan
- Triggered by:
temporal/t.rast3d.algebra/testsuite/test_raster3d_algebra.py,temporal/t.rast.to.rast3/testsuite/test_strds_to_rast3.py,temporal/t.rast3d.univar/testsuite/test_t_rast3d_univar.py, hits: 183 (28 test files) - Message:
signed integer overflow: 72057594037927936 * 256 cannot be represented in type 'long' - Likely cause (hypothesis):
d *= 256runs once more than needed on the last byte iteration, overflowinglong(256^8); the value is not used afterwards. Likely benign in practice. Hint: avoid the final multiply. - Repro (gunittest):
cd temporal/t.rast3d.algebra/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_raster3d_algebra.py(gunittest environment below) - Repro (pytest):
v/bin/python -m pytest -p no:cacheprovider --timeout=600 general/g.findfile/tests/test_g_findfile.pyfrom the repo root (pytest environment below); also seen in 8 other pytest file(s) - Raw log:
raw-logs/temporal_t.rast3d.algebra_testsuite_test_raster3d_algebra.py/ubsan.3879 - Verification (verified: benign):
dis not read after the loop and the encoded bytes are right for positive values. Side observation (separate issue, decoder not checked): a negative long (-2) encodes asfe 00 00 ..., i.e. it is not sign-extended. - Minimal repro:
verify/numeric/m02.c - Also seen in pytest run:
raw-logs-pytest/general_g.findfile_tests_test_g_findfile.py/ubsan.25033
Trimmed stack:
lib/raster3d/long.c:27 Rast3d_long_encode
lib/raster3d/index.c:107 Rast3d_flush_index
lib/raster3d/close.c:82 close_cell_new
lib/raster3d/close.c:153 Rast3d_close
raster/r.mapcalc/map3.c:639 close_output_map
raster/r.mapcalc/evaluate.c:514 execute
M02 float-cast-overflow in raster/r.sim/simlib/hydro.c:112
- Sanitizer: UBSan
- Triggered by:
raster/r.sim/r.sim.water/tests/r_sim_water_test.py, hits: 1 - Message:
-nan is outside the range of representable values of type 'int' - Likely cause (hypothesis):
gen = rwalk * si / sisumis NaN (likelysisum== 0), then(int)gencasts NaN to int. Hint: guard against a zero/NaNsisum. - Repro (pytest):
v/bin/python -m pytest -p no:cacheprovider --timeout=600 raster/r.sim/r.sim.water/tests/r_sim_water_test.pyfrom the repo root (pytest environment below) - Raw log:
raw-logs-pytest/raster_r.sim_r.sim.water_tests_r_sim_water_test.py/ubsan.28296 - Verification (verified: benign on x86-64): Trigger is
rain_value=0:sisumis 0,genis NaN,(int)NaNis INT_MIN on x86-64, the walker loop runs zero times and depth is all 0 (the expected result). Relies on x86 behaviour. - Minimal repro:
verify/numeric/m03.sh
Trimmed stack:
raster/r.sim/simlib/hydro.c:112 main_loop
raster/r.sim/r.sim.water/main.c:572 main
M03 signed-integer-overflow in raster/r.stats/stats.c:134
- Sanitizer: UBSan
- Triggered by:
raster/r.kappa/testsuite/test_r_kappa.py,raster/r.coin/tests/test_coin.py, hits: 3 (2 test files) - Message:
signed integer overflow: -2147483647 * 89 cannot be represented in type 'int' - Likely cause (hypothesis): The hash
hash * HASHMOD + valueintentionally wraps around for large category values; overflow of signed int is UB, though the result is only used as a bucket index. Hint: use unsigned arithmetic. - Repro (gunittest):
cd raster/r.kappa/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_r_kappa.py(gunittest environment below) - Repro (pytest):
v/bin/python -m pytest -p no:cacheprovider --timeout=600 raster/r.coin/tests/test_coin.pyfrom the repo root (pytest environment below) - Raw log:
raw-logs/raster_r.kappa_testsuite_test_r_kappa.py/ubsan.29938 - Verification (verified: hash overflow benign; see H06): The hash only needs to be deterministic; the tree comparison uses
values[]. The INT_MIN case is a real bug, listed as H06. - Minimal repro:
verify/numeric/m04.sh - Also seen in pytest run:
raw-logs-pytest/raster_r.coin_tests_test_coin.py/ubsan.18019
Trimmed stack:
raster/r.stats/stats.c:134 update_cell_stats
raster/r.stats/cell_stats.c:63 cell_stats
raster/r.stats/main.c:417 main
M04 signed-integer-overflow in raster/r.stats/stats.c:134
- Sanitizer: UBSan
- Triggered by:
raster/r.kappa/testsuite/test_r_kappa.py,raster/r.coin/tests/test_coin.py, hits: 3 (2 test files) - Message:
signed integer overflow: -2147483559 + -2147483647 cannot be represented in type 'int' - Likely cause (hypothesis): The hash
hash * HASHMOD + valueintentionally wraps around for large category values; overflow of signed int is UB, though the result is only used as a bucket index. Hint: use unsigned arithmetic. - Repro (gunittest):
cd raster/r.kappa/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_r_kappa.py(gunittest environment below) - Repro (pytest):
v/bin/python -m pytest -p no:cacheprovider --timeout=600 raster/r.coin/tests/test_coin.pyfrom the repo root (pytest environment below) - Raw log:
raw-logs/raster_r.kappa_testsuite_test_r_kappa.py/ubsan.29938 - Verification (verified: hash overflow benign; see H06): The hash only needs to be deterministic; the tree comparison uses
values[]. The INT_MIN case is a real bug, listed as H06. - Minimal repro:
verify/numeric/m04.sh - Also seen in pytest run:
raw-logs-pytest/raster_r.coin_tests_test_coin.py/ubsan.18019
Trimmed stack:
raster/r.stats/stats.c:134 update_cell_stats
raster/r.stats/cell_stats.c:63 cell_stats
raster/r.stats/main.c:417 main
M05 signed-integer-overflow in raster/r.texture/h_measure.c:373
- Sanitizer: UBSan
- Triggered by:
raster/r.texture/testsuite/test_rtexture_parallel.py,raster/r.texture/testsuite/test_rtexture.py, hits: 2 (2 test files) - Message:
signed integer overflow: 47961 * 47961 cannot be represented in type 'int' - Likely cause (hypothesis):
tmp * tmpis computed inintwheretmpis the squared tone range; for wide ranges (47961^2) it overflows before the float division. Hint: use double/float fortmp. - Repro (gunittest):
cd raster/r.texture/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_rtexture_parallel.py(gunittest environment below) - Raw log:
raw-logs/raster_r.texture_testsuite_test_rtexture_parallel.py/ubsan.9006 - Verification (verified: output changes slightly): With a double divisor (patched copy in
verify/numeric/rt/) 1401 of 331906 cells differ (0.42%) onlsat7_2002_80,size=3,method=dv; max |diff| 5.5e-5. Only windows with tone range >= 216 are affected. - Minimal repro:
verify/numeric/m06.sh,m06b.sh
Trimmed stack:
raster/r.texture/h_measure.c:373 f10_dvar
raster/r.texture/execute.c:132 execute_texture
raster/r.texture/main.c:318 main
M06 signed-integer-overflow in raster/r.univar/r.univar_main.c:516
- Sanitizer: UBSan
- Triggered by:
general/g.rename/testsuite/test_overwrite.py, hits: 3 - Message:
signed integer overflow: 50000 * 50000 cannot be represented in type 'int' - Likely cause (hypothesis):
val * valis computed inintbefore being added to the floating-pointsumsq; values above ~46340 overflow. Cast to double before multiplying. - Repro (gunittest):
cd general/g.rename/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_overwrite.py(gunittest environment below) - Raw log:
raw-logs/general_g.rename_testsuite_test_overwrite.py/ubsan.3942 - Verification (verified: WRONG OUTPUT): For CELL maps any |value| >= 46341 wraps, so
sumsq, variance, stddev and coeff_var are wrong (mean, sum, min, max are fine). Example: 100 cells, five rows of 10 cells = 50000, rest 3:r.univar -ggives variance=0 on the CELL map vs 624925002.25 on its double copy.r3.univaris not affected. - Minimal repro:
verify/numeric/m07.sh
Trimmed stack:
raster/r.univar/r.univar_main.c:516 process_raster.omp_outlined_debug__
raster/r.univar/r.univar_main.c:399 process_raster.omp_outlined
raster/r.univar/r.univar_main.c:399 process_raster
raster/r.univar/r.univar_main.c:321 main
L01 array-bounds in lib/gis/aprintf.c:198
- Sanitizer: UBSan
- Triggered by:
vector/v.info/testsuite/test_vinfo.py, hits: 1 - Message:
index -1 out of bounds for type 'char[23]' - Likely cause (hypothesis):
char *c = CONVS - 1;forms a pointer one before the start of the string literal, then pre-increments it; the sanitizer flags the index -1. Probably benign on common platforms. Hint: restructure the loop to avoid the out-of-range pointer. - Repro (gunittest):
cd vector/v.info/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_vinfo.py(gunittest environment below) - Raw log:
raw-logs/vector_v.info_testsuite_test_vinfo.py/ubsan.19755 - Verification (verified: formal UB only):
CONVS - 1is out-of-bounds pointer arithmetic, but it is pre-incremented before the first read, so no memory outside the string is accessed. Severity lowered from MEDIUM to LOW. - Minimal repro:
v.infoon any vector (seeverify/terraflow/v.out)
Trimmed stack:
lib/gis/aprintf.c:198 oaprintf
lib/gis/aprintf.c:427 G_vsaprintf
lib/gis/aprintf.c:519 G_saprintf
vector/v.info/print.c:626 print_info
vector/v.info/main.c:97 main
L02 nonnull-attribute in lib/gis/ls.c:116
- Sanitizer: UBSan
- Triggered by:
temporal/t.rast.to.rast3/testsuite/test_strds_to_rast3.py,scripts/db.in.ogr/testsuite/test_db_in_ogr.py,vector/v.out.ogr/testsuite/test_v_out_ogr.py, hits: 115 (29 test files) - Message:
null pointer passed as argument 1, which is declared to never be null - Likely cause (hypothesis):
qsort(dir_listing, n, ...)is called withdir_listing == NULLwhen the directory has no (visible) entries (n = 0). Passing NULL to qsort is UB even with a zero count. Hint: skip qsort when n == 0. - Repro (gunittest):
cd temporal/t.rast.to.rast3/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_strds_to_rast3.py(gunittest environment below) - Repro (pytest):
v/bin/python -m pytest -p no:cacheprovider --timeout=600 python/grass/tools/tests/grass_tools_session_tools_pack_test.pyfrom the repo root (pytest environment below); also seen in 4 other pytest file(s) - Raw log:
raw-logs/temporal_t.rast.to.rast3_testsuite_test_strds_to_rast3.py/ubsan.6308 - Verification (verified: formal UB only):
G_ls2on an existing empty directory callsqsort(NULL, 0). All callers checked tolerate the NULL return. - Minimal repro:
g.remove -f type=vector pattern='*'in a mapset with an emptyvector/directory (verify/nullptr/l01.sh) - Also seen in pytest run:
raw-logs-pytest/python_grass_tools_tests_grass_tools_session_tools_pack_test.py/ubsan.11965
Trimmed stack:
lib/gis/ls.c:116 G_ls2
general/g.remove/main.c:239 main
L03 nonnull-attribute in lib/gis/plot.c:455
- Sanitizer: UBSan
- Triggered by:
scripts/v.rast.stats/testsuite/test_v_rast_stats.py, hits: 1 - Message:
null pointer passed as argument 1, which is declared to never be null - Likely cause (hypothesis):
qsort(st->P, st->np, ...)with a NULLst->Pwhen the polygon has no edge points (np = 0). Hint: skip qsort when np == 0. - Repro (gunittest):
cd scripts/v.rast.stats/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_v_rast_stats.py(gunittest environment below) - Raw log:
raw-logs/scripts_v.rast.stats_testsuite_test_v_rast_stats.py/ubsan.25680 - Verification (verified: formal UB only): An area that crosses no pixel-row centre gives
np == 0, sost->Pwas never allocated. The odd-point check does not matter. - Minimal repro: 2x1 m polygon at 10 m resolution then
v.to.rast(verify/nullptr/l02.sh)
Trimmed stack:
lib/gis/plot.c:455 G_plot_polygon
vector/v.to.rast/do_areas.c:81 do_areas
vector/v.to.rast/vect2rast.c:189 vect_to_rast
vector/v.to.rast/main.c:162 main
L04 pointer-overflow (null + offset) in raster/r.fill.stats/main.c:595
- Sanitizer: UBSan
- Triggered by:
raster/r.fill.stats/testsuite/test_r_fill_stats.py, hits: 4 - Message:
applying non-zero offset 4 to null pointer - Likely cause (hypothesis):
err_output++advances a pointer that is NULL when the error map is not requested (write_errfalse). Advance only whenwrite_erris set. - Repro (gunittest):
cd raster/r.fill.stats/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_r_fill_stats.py(gunittest environment below) - Raw log:
raw-logs/raster_r.fill.stats_testsuite_test_r_fill_stats.py/ubsan.26050 - Verification (verified: formal UB only): Without
error=,err_output++runs on NULL; every dereference is underwrite_err. - Minimal repro:
r.fill.stats input=inp output=out1(verify/nullptr/l03.sh) - Possibly related: #4472 (GCC nonnull in r.fill.stats; not the same line)
Trimmed stack:
raster/r.fill.stats/main.c:595 interpolate_row
raster/r.fill.stats/main.c:1266 main
L05 pointer-overflow (null + offset) in raster/r.sun/main.c:1955
- Sanitizer: UBSan
- Triggered by:
raster/r.sun/testsuite/test_rsun.py, hits: 5 - Message:
applying non-zero offset 501 to null pointer - Likely cause (hypothesis): Same pattern as the
main.c:1972finding:horizonarrayis NULL with a non-zero offset (call tolumcline2). Hint: same as above. - Repro (gunittest):
cd raster/r.sun/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_rsun.py(gunittest environment below) - Raw log:
raw-logs/raster_r.sun_testsuite_test_rsun.py/ubsan.7394 - Verification (verified: formal UB only): Pointer is only formed, read only under
useHorizonData()atrsunlib.c:342. - Minimal repro:
r.sun ... incidout=inc nprocs=2without horizon input (verify/nullptr/l04d.sh)
Trimmed stack:
raster/r.sun/main.c:1955 calculate.omp_outlined_debug__
raster/r.sun/main.c:1801 calculate.omp_outlined
raster/r.sun/main.c:1801 calculate
raster/r.sun/main.c:819 main
L06 pointer-overflow (null + offset) in raster/r.sun/main.c:1972
- Sanitizer: UBSan
- Triggered by:
raster/r.sun/testsuite/test_rsun.py, hits: 8 - Message:
applying non-zero offset 301 to null pointer - Likely cause (hypothesis):
horizonarray + shadowoffsetis evaluated whilehorizonarrayis NULL (no horizon input) andshadowoffsetis non-zero. Hint: only form the pointer when horizon data is used. - Repro (gunittest):
cd raster/r.sun/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_rsun.py(gunittest environment below) - Raw log:
raw-logs/raster_r.sun_testsuite_test_rsun.py/ubsan.6950 - Verification (verified: formal UB only): Same as :1955.
- Minimal repro:
r.sun ... beam_rad=br nprocs=2without horizon input (verify/nullptr/l04d.sh)
Trimmed stack:
raster/r.sun/main.c:1972 calculate.omp_outlined_debug__
raster/r.sun/main.c:1801 calculate.omp_outlined
raster/r.sun/main.c:1801 calculate
raster/r.sun/main.c:819 main
L07 nonnull-attribute in vector/v.extract/extract.c:35
- Sanitizer: UBSan
- Triggered by:
scripts/v.dissolve/testsuite/test_v_dissolve.py,scripts/v.dissolve/tests/v_dissolve_layers_test.py,scripts/v.dissolve/tests/v_dissolve_test.py, hits: 19 (5 test files) - Message:
null pointer passed as argument 2, which is declared to never be null - Likely cause (hypothesis):
bsearch(&cat, cats_array, ncats_array, ...)with NULLcats_arrayand zero count. Hint: return 0 early when ncats_array == 0. - Repro (gunittest):
cd scripts/v.dissolve/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_v_dissolve.py(gunittest environment below) - Repro (pytest):
v/bin/python -m pytest -p no:cacheprovider --timeout=600 scripts/v.dissolve/tests/v_dissolve_layers_test.pyfrom the repo root (pytest environment below); also seen in 3 other pytest file(s) - Raw log:
raw-logs/scripts_v.dissolve_testsuite_test_v_dissolve.py/ubsan.23538 - Verification (verified: formal UB only): Category array is NULL when the category list is empty; called with nmemb == 0.
- Minimal repro:
v.extract input=soils_wake output=e1 where="1=0"(verify/nullptr/l06.sh) - Also seen in pytest run:
raw-logs-pytest/scripts_v.dissolve_tests_v_dissolve_layers_test.py/ubsan.1292
Trimmed stack:
vector/v.extract/extract.c:35 in_list
vector/v.extract/extract.c:349 extract_line
vector/v.extract/main.c:422 main
L08 nonnull-attribute in vector/v.extract/extract.c:285
- Sanitizer: UBSan
- Triggered by:
scripts/v.dissolve/testsuite/test_v_dissolve.py,scripts/v.dissolve/tests/v_dissolve_layers_test.py,scripts/v.dissolve/tests/v_dissolve_test.py, hits: 19 (5 test files) - Message:
null pointer passed as argument 1, which is declared to never be null - Likely cause (hypothesis):
qsort(cats_array, ncats_array, ...)withcats_arrayNULL when no categories were collected. Hint: skip when ncats_array == 0. - Repro (gunittest):
cd scripts/v.dissolve/testsuite && grass --tmp-mapset <nc_spm_08_grass7> --exec python test_v_dissolve.py(gunittest environment below) - Repro (pytest):
v/bin/python -m pytest -p no:cacheprovider --timeout=600 scripts/v.dissolve/tests/v_dissolve_layers_test.pyfrom the repo root (pytest environment below); also seen in 3 other pytest file(s) - Raw log:
raw-logs/scripts_v.dissolve_testsuite_test_v_dissolve.py/ubsan.23538 - Verification (verified: formal UB only): Same as :35.
- Minimal repro:
v.extract input=soils_wake output=e4 type=area -r(verify/nullptr/l06.sh) - Also seen in pytest run:
raw-logs-pytest/scripts_v.dissolve_tests_v_dissolve_layers_test.py/ubsan.1292
Trimmed stack:
vector/v.extract/extract.c:285 extract_line
vector/v.extract/main.c:422 main
Additional new[]/delete mismatches (found by reading, not exercised)
Same root family as H01-H04 (new char[] released with scalar delete), found by reading include/grass/iostream/*.h and raster/r.terraflow/. No delete on strdup/malloc memory was found. Several sites are dead debug code (if (0)).
ami_sort.h:107(insideSORT_DEBUG, dead)replacementHeap.h:127(operator<<, debug printing)replacementHeap.h:202(RHEAP_DEBUG, dead)replacementHeapBlock.h:129(operator<<) and:204(RBHEAP_DEBUG, dead)embuffer.h:494, 516, 769, 862(delete name[i];,SAVE_MEMORYis defined, not exercised by r.terraflow)embuffer.h:555(delete fooname;, only underEMBUF_CHECK_NAME, commented out)
Related, not sanitizer findings (leaks, detection disabled): name strings never freed at ami_sort.h:122, r.terraflow/grass2str.h:48,164,247,321,411 and nodata.cpp:320.
Filtered out
- Third-party only (no GRASS frame in first 5): 75
- Test-only helper code: 0
- Duplicate hits of listed findings: 4506
- Leak reports: 0 (leak detection disabled)
- Sanitizer/LD_PRELOAD artifacts: 2 aborted test files (
lib/imagery/testsuite/test_imagery_sigfile.py,test_imagery_sigsetfile.py): the tests call ctypeslibc.free()on memory from GRASS code that ASan allocated, giving glibcmunmap_chunk(): invalid pointer; this is an artifact of the preload setup and the remaining tests in those files did not run.
Not covered
- pytest tests that failed or were skipped for environment reasons (see Summary) only ran partially; tests marked slow were not deselected.
pygrass/testsare ignored by the repo pytest config. - Dataset is the basic
nc_spm_08_grass7sample, not the full one; several tests failed for missing maps (e.g.zipcodes,streets,landuse,schools) and are weaker coverage. 46 files exited 1 (some are intentional failures fromgunittestsample code, some need missing data or the GUI/wx, PDAL/LAS or network access). - Not built: GUI (wxPython), PDAL/LAS modules (
v.in.pdal,v.in.lidar,r.in.pdal, ...), OpenGL, X11 drivers, docs. - Disabled checks: leak detection (
detect_leaks=0),-fno-sanitize=function. No TSan/MSan. - Python-side code was run under the preloaded ASan runtime but was not itself instrumented.
- ASan reports were written to
ubsan.<pid>files rather thanasan.<pid>(the combined runtime honours one log path); the parser reads both.
Reproduce this run
pytest (after the build below; the repo's pyproject.toml needs pytest >= 8.2, so use a venv):
python -m venv --system-site-packages v && v/bin/pip install 'pytest>=8.2' pytest-timeout
export PATH=$PWD/build/output/bin:$PATH GRASS_CONFIG_DIR=$PWD/gconf LOGDIR=$PWD/ptlogs/<test-id>
mkdir -p $GRASS_CONFIG_DIR $LOGDIR
export ASAN_OPTIONS=detect_leaks=0:halt_on_error=0:detect_stack_use_after_return=1:log_path=$LOGDIR/asan
export UBSAN_OPTIONS=print_stacktrace=1:log_path=$LOGDIR/ubsan
export LD_PRELOAD=$(clang -print-file-name=libclang_rt.asan-x86_64.so)
export PYTHONPATH="$(grass --config python_path):$PYTHONPATH" LD_LIBRARY_PATH="$(grass --config path)/lib:$LD_LIBRARY_PATH"
cd grass # repo root
v/bin/python -m pytest -p no:cacheprovider --timeout=600 <path/to/*_test.py>
gunittest:
# Build (throwaway dir, nothing installed)
apt install clang libclang-rt-18-dev libomp-18-dev libgdal-dev libproj-dev libgeos-dev libfftw3-dev liblapacke-dev libopenblas-dev libnetcdf-dev libpq-dev libzstd-dev libtiff-dev libcairo2-dev libfreetype-dev gettext flex bison ninja-build
export ASAN_OPTIONS=detect_leaks=0 CC=clang CXX=clang++
export CFLAGS="-g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined -fsanitize-recover=address,undefined -fno-sanitize=function" CXXFLAGS="$CFLAGS"
export LDFLAGS="-fsanitize=address,undefined -shared-libasan -Wl,-rpath,$(dirname $(clang -print-file-name=libclang_rt.asan-x86_64.so))"
cmake -S grass -B build -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DWITH_PDAL=OFF -DWITH_GUI=OFF -DWITH_X11=OFF -DWITH_OPENGL=OFF -DWITH_DOCS=OFF
ninja -C build
# Run one test file (repeat for each; 4 in parallel)
export PATH=$PWD/build/output/bin:$PATH GRASS_CONFIG_DIR=$PWD/gconf LOGDIR=$PWD/logs/<test-id>
mkdir -p $GRASS_CONFIG_DIR $LOGDIR
export ASAN_OPTIONS=detect_leaks=0:halt_on_error=0:detect_stack_use_after_return=1:log_path=$LOGDIR/asan
export UBSAN_OPTIONS=print_stacktrace=1:log_path=$LOGDIR/ubsan
export LD_PRELOAD=$(clang -print-file-name=libclang_rt.asan-x86_64.so)
cd <dir>/testsuite # copy of the repo testsuite dir
timeout 600 grass --tmp-mapset ~/grassdata/nc_spm_08_grass7 --exec python test_xxx.py
Build invocation changes needed (no source changes): clang needs -shared-libasan at link time so the LD_PRELOADed runtime is not a second ASan copy, plus an rpath to the clang runtime directory for build-time helper tools; libomp-18-dev for OpenMP; GUI/PDAL disabled.
- Dominant language
- C
- Stars
- 1.2k
- Forks
- 447
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 62
Getting set up
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from OSGeo/grass
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 83/100
Maintainers usually reply within 1 day
-
[Bug] v.db.addtable expands $MAP only when it is a directoryPossibly taken @alteralto claimed this 17 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
[Bug] grass.pygrass: Cats.delete(0) deletes all layer categories instead of category 0Possibly taken @Mohit-Ak claimed this 22 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
[Bug] macOS: Startup crashes with StopIteration when ~/Documents is not readable (macOS privacy protection)Possibly taken @NIne-WIngEd claimed this 28 days ago. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
OSGeo/grass#8027 · 3 comments ·
Maintainers usually reply within 1 day
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
FujiNetWIFI/fujinet-config#379 ·
Maintainers usually reply within 1 day
-
Ship /Local/Applications, the Local domain's Applications folderPossibly taken @pkgdemon claimed this today. Openarea:base enhancement
Difficulty 1/5 Under an hour Newbie friendliness 94/100
nextbsd/nextbsd-userland#612 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
zephyrproject-rtos/zephyr#125543 ·
Maintainers usually reply within 2 days
-
good first issue needs-triage
Difficulty 1/5 Under an hour Newbie friendliness 88/100
InauguralSystems/EigenScript#2347 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
colbymchenry/codegraph#3277 ·
Maintainers usually reply within 1 day