Skip to content

fix(pipeline): skip the function sort when the graph has no functions - #2083

Merged
DeusData merged 1 commit into
mainfrom
fix/semantic-edges-qsort-null-guard
Sep 7, 2026
Merged

DeusData merged 1 commit into
mainfrom
fix/semantic-edges-qsort-null-guard

Conversation

@DeusData

@DeusData DeusData commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Defect

phase1_scan_functions() in src/pipeline/pass_semantic_edges.c sorts the collected Function/Method node pointers (qsort(node_ptrs, (size_t)func_count, ...)) so the func order is deterministic. When the graph has no Function or Method node at all, node_ptrs is never allocated and the call is qsort(NULL, 0, ...). The func_count <= 0 early-out sits in phase 1b (phase1b_decode_and_build), which runs only after phase 1 has returned, so it never guards this call. A NULL qsort base is undefined behaviour; glibc declares the parameter nonnull, so UBSan reports it.

Reachability

Any struct/class-only or config-only project indexed in full/moderate mode. The existing pipeline_docstring_go_class test (a Go struct-only fixture) already reaches the call on the Linux leg today.

Which lane flags it

  • Linux is the binding lane: the arm64 test container (gcc 13.3 + glibc; scripts/test.sh --suites pipeline CC=gcc CXX=g++ BUILD_DIR=build/linux-arm64) reports src/pipeline/pass_semantic_edges.c:996:5: runtime error: null pointer passed as argument 1, which is declared to never be null with the stack phase1_scan_functions <- cbm_pipeline_pass_semantic_edges <- predump_sem <- cbm_pipeline_run.
  • macOS cannot flag it: the SDK's qsort (usr/include/_stdlib.h) carries _Nonnull only on the comparator, so both the default ASan/UBSan build and the Homebrew LLVM diag lane are silent. Verified with a stand-alone qsort(NULL, 0, ...) probe under Apple clang 21 and Homebrew clang 22.1.8: no report from either.
  • The harness has no UBSan gating (no UBSAN_OPTIONS, no -fno-sanitize-recover, no report scan), so a recoverable report prints and the suite still passes -- which is why this went unnoticed. That is being recorded separately as a test-infra finding.

Fix

Guard the sort on func_count > 0 (three lines, main's style). The re-derivation loop after it is already a no-op for an empty scan.

Regression test

pipeline_semantic_edges_no_functions (suite pipeline) indexes a Go struct-only fixture (type Widget struct{}) through the full pipeline so pass_semantic_edges runs, and pins the fixture to what it claims: Struct >= 1, Function = 0, Method = 0.

Verification

  • RED on origin/main (aa44c28), Linux container, UBSAN_OPTIONS=halt_on_error=1 with the one unrelated pre-existing report site suppressed (the vendored objectscript_udl external scanner passing a NULL source to memcpy, which surfaces as scanner.c:122 and bits/string_fortified.h:29): the pipeline suite aborts at pass_semantic_edges.c:996, exit 1. UBSan reports each site once per process and pipeline_docstring_go_class reaches it first in suite order; with the new test registered first for one diagnostic run, the abort is owned by pipeline_semantic_edges_no_functions.
  • GREEN with the fix, same lane: recoverable run 275 passed with zero :996 reports (only the two objectscript_udl reports remain); halt_on_error=1 run completes, 275 passed, exit 0.
  • macOS default lane: build/c/test-runner pipeline extraction 617 passed, zero UBSan output (before and after, as expected on this SDK).
  • make -f Makefile.cbm lint-ci clean; bash scripts/check-no-test-skips.sh OK; no raw fopen(, no SKIP/NOLINT, ASCII-only additions.

Distilled from #1810 with co-author credit to @ysfsmet.

Refs #1810

phase1_scan_functions() in pass_semantic_edges.c sorts the collected
Function/Method node pointers so the func order is deterministic. When
the graph has no Function or Method node at all -- a struct-only or
config-only project -- node_ptrs is never allocated and the call becomes
qsort(NULL, 0, ...). The func_count <= 0 early-out lives in phase 1b,
which runs only after phase 1 has returned, so it never guards this call.

A NULL qsort base is undefined behaviour. glibc declares the parameter
nonnull, so UBSan on the Linux leg reports "null pointer passed as
argument 1, which is declared to never be null" at the call. The macOS
SDK carries no such attribute, so neither the default sanitizer build nor
the Homebrew LLVM diag lane can flag it there. The existing
pipeline_docstring_go_class test already reaches the call on Linux; the
harness never gated on recoverable UBSan reports, so it went unnoticed.

Guard the sort on func_count > 0. The new test
pipeline_semantic_edges_no_functions indexes a Go struct-only fixture
through the full pipeline and pins the fixture to what it claims
(Struct >= 1, Function = Method = 0). On the Linux leg with
UBSAN_OPTIONS=halt_on_error=1 the pipeline suite aborts at the qsort
call before the fix and completes with it.

Distilled from #1810 with co-author credit.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-authored-by: Yusuf S. Demirci <ysf-samet@outlook.com>
@DeusData
DeusData merged commit 8ec859b into main Sep 7, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant