Skip to content

cl: preserve required nil dereference checks - #2256

Open
cpunion wants to merge 8 commits into
xgo-dev:mainfrom
cpunion:codex/goroot-unused-nil-deref-20260801
Open

cl: preserve required nil dereference checks#2256
cpunion wants to merge 8 commits into
xgo-dev:mainfrom
cpunion:codex/goroot-unused-nil-deref-20260801

Conversation

@cpunion

@cpunion cpunion commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • preserve required nil checks when LLVM could otherwise elide an unused, aggregate, multiword, or derived-address load
  • follow FieldAddr and IndexAddr chains back to the actual pointer base, including indexed address-of selectors
  • check promoted pointer-method receivers derived from a nil outer pointer while preserving Go behavior for direct nil pointer receivers
  • keep nil-only panic paths from extending pointer liveness on the normal path
  • conservatively eliminate redundant guards only after a same-block non-nil store or an equivalent reload dominated by an explicit nil comparison
  • remove the GOROOT xfails for fixedbugs/issue38496.go and nilptr2.go

The implementation does not special-case either upstream test. These cases share one root cause: the effective load address may be non-nil, or the load may disappear entirely, even though evaluating the original Go expression must still panic at its nil pointer base.

The local non-nil proof deliberately stops at calls, unknown aliases, nil overwrites, joins, and loops. This is also required for the conservative ESP32-C3 collector: unnecessary pointer checks can otherwise spill already-dead pointers and retain objects, while removing a check without a valid memory fact would break Go nil-panic behavior.

The branch is based on current xgo-dev main (60c30f2ff). This PR does not change CI configuration or timing.

Validation

  • compiler regressions cover direct, nested, derived, address-of, promoted, explicit, bound receiver, and dominating == nil / != nil forms
  • counterexamples retain their guards after a call, possible alias store, nil overwrite, or checked-value alias store
  • LLGo runtime nil-dereference suite passes, including the review counterexamples and a direct nil receiver control case
  • the only changed existing LLVM snapshot, tprecurfn, passes; affected testdata/testgo/testrt cases pass with unchanged Go source bodies
  • GOROOT nilptr2.go and fixedbugs/issue38496.go pass on Go 1.26.5; the nil group also previously passed on Go 1.24.11 and 1.25.7
  • the full ESP32-C3 gc-runtime suite passes under the same Espressif QEMU used by CI, including InterfaceLiveness, NestedStructPointers, and MultipleCyclesDisjoint
  • focused cl tests and git diff --check pass

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: preserve nil checks for unused dereferences

The core change is correct and well-tested: collapsing the large-value / slice-conversion special cases into a single unconditional guard path ensures _ = *p still panics on a nil pointer, and the added compiler-IR and runtime regression tests (TestUnusedDerefEmitsNilGuard, TestUnusedNilDerefOperationsPanic) lock in the behavior. The retained skipUnusedArrayDeref carve-out is a correct, spec-mandated exception (ranging over / len of a *[N]T must not dereference the pointer). No correctness, security, or documentation-accuracy issues were found — the removed CHECK lines in cvar/varinit and the two dropped issue38496.go xfails are consistent with the new behavior.

Two non-blocking quality/performance findings are noted inline. Both concern emitting nil guards that are provably dead in specific cases; neither affects correctness.

Optional follow-up: test coverage for the collapsed branches

The new tests cover a small direct pointer and an array-element load, but not the two cases whose dedicated handling was removed — an unused deref of a large non-pointer struct, and an unused zero-length slice-to-array-conversion deref. Adding those would lock in the merged branch's behavior for exactly the paths that were deleted.

Comment thread cl/compile.go
Comment thread cl/compile.go
@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.36842% with 29 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cl/instr.go 91.56% 19 Missing and 8 partials ⚠️
cl/compile.go 95.55% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

3528365edf99 | workflow run | long-term charts

Program measurements

Platform Workload File size vs main Build vs main Run vs main
Linux cprintf 18544 B new 370.408 ms new 1.353 ms new
Linux fmtprintf 2241992 B new 3.399 s new 2.723 ms new
Linux println 72944 B new 362.387 ms new 1.725 ms new
macOS cprintf 84672 B new 572.284 ms new 4.663 ms new
macOS fmtprintf 2378848 B new 4.444 s new 22.556 ms new
macOS println 125904 B new 584.243 ms new 6.628 ms new
Core language and compiler benchmarks
Platform Benchmark ns/op vs main
Linux BenchmarkLookupPCRandom 12.320 ns/op new
Linux BenchmarkMergeCompilerFlags 145.100 ns/op new
Linux BenchmarkMergeLinkerFlags 94.070 ns/op new
Linux BenchmarkChannelBuffered 36.630 ns/op new
Linux BenchmarkChannelHandoff 24156 ns/op new
Linux BenchmarkDefer 43.900 ns/op new
Linux BenchmarkDirectCall 1.759 ns/op new
Linux BenchmarkGlobalRead 1.757 ns/op new
Linux BenchmarkGlobalWrite 2.806 ns/op new
Linux BenchmarkGoroutine 30456 ns/op new
Linux BenchmarkInterfaceCall 9.489 ns/op new
Linux BenchmarkRuntimeGetG 2.110 ns/op new
macOS BenchmarkLookupPCRandom 13.740 ns/op new
macOS BenchmarkMergeCompilerFlags 124.200 ns/op new
macOS BenchmarkMergeLinkerFlags 79.890 ns/op new
macOS BenchmarkChannelBuffered 26.190 ns/op new
macOS BenchmarkChannelHandoff 8947 ns/op new
macOS BenchmarkDefer 31.850 ns/op new
macOS BenchmarkDirectCall 1.178 ns/op new
macOS BenchmarkGlobalRead 1.357 ns/op new
macOS BenchmarkGlobalWrite 1.419 ns/op new
macOS BenchmarkGoroutine 30459 ns/op new
macOS BenchmarkInterfaceCall 4.934 ns/op new
macOS BenchmarkRuntimeGetG 2.346 ns/op new

No main baseline exists yet; all metrics are marked new.

Warning

  • The rendered benchmark data could not be pushed.

@cpunion

cpunion commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Handled both review findings in 50c5332b1:

  • known-non-nil local allocations and ssa:wrapnilchk operands no longer emit an unused nil guard
  • effectful array-pointer dereferences no longer emit the same guard twice
  • added the requested IR counterexamples, asserting exactly 0 and exactly 1 AssertNilDeref calls respectively

Fresh validation passes: focused compiler/fixture tests, the LLGo runtime nil-deref regression, and upstream fixedbugs/issue38496.go without xfail on Go 1.24.11, 1.25.0, and 1.26.5.

@cpunion
cpunion force-pushed the codex/goroot-unused-nil-deref-20260801 branch from 50c5332 to 3220195 Compare August 1, 2026 20:08
@cpunion cpunion changed the title cl: preserve nil checks for unused dereferences cl: preserve required nil dereference checks Aug 1, 2026
@cpunion

cpunion commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Expanded and rebased this PR in 3220195ef to cover the full shared nil-dereference root found in nilptr2.go:

  • aggregate and multiword loads
  • FieldAddr/IndexAddr base chains, including indexed address-of expressions
  • promoted, explicit, and bound pointer-method receivers derived from a nil outer pointer
  • the review counterexamples as LLGo runtime and compiler tests

Direct nil pointer receivers remain legal and are covered by a control test. The final implementation also removes redundant checks on already-derived addresses.

Fresh validation passes for the focused compiler suite, all new LLGo runtime cases, all affected LLVM snapshots, and upstream nilptr2.go with Go 1.24.11, 1.25.7, and 1.26.5. The PR contains no CI configuration changes.

@cpunion

cpunion commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

The first CI run exposed a conservative-GC interaction rather than a nil-check correctness failure: both Ubuntu LLGo matrix jobs failed the ESP32-C3 gc-runtime smoke tests (InterfaceLiveness, NestedStructPointers, and MultipleCyclesDisjoint) because the newly emitted helper calls could spill the checked pointer and keep it conservatively reachable on the non-nil path.

Fixed in a01b60e2f by:

  • branching to the panic helper only when the pointer is nil, leaving the normal path call-free
  • recognizing values already proven non-nil by a dominating == nil or != nil branch
  • adding compiler regressions for both comparison forms and both operand orders
  • synchronizing only the affected LLVM FileCheck blocks across _testgo, _testdata, and _testrt

Fresh local validation passes: full ssa, focused cl, the LLGo nil-dereference runtime suite, and both removed GOROOT xfails on Go 1.24.11, 1.25.7, and 1.26.5. The 43 updated golden files have identical non-CHECK Go source bodies. No CI or timeout configuration was changed.

@cpunion

cpunion commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

The prior branch-only change fixed InterfaceLiveness, but the full CI-equivalent ESP32-C3 run showed two more conservative-GC regressions: NestedStructPointers retained one object and MultipleCyclesDisjoint retained cycle B.

3528365ed completes the shared fix by proving only two narrow non-nil fact classes: a same-block pointer load following a non-nil store, and an equivalent reload dominated by an explicit nil comparison along a single-predecessor memory-unchanged path. Calls, unknown aliases, nil stores, joins, and loops stop the proof; the corresponding counterexamples assert that one guard remains.

With the exact Espressif QEMU setup used by CI, the complete gc-runtime program now reports PASS for all tests and ends in OK, including all three previously failing cases. No CI timeout or workflow setting was changed.

@cpunion

cpunion commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the patch-coverage failure in 836251d70. The missing lines were isolated to the new ssa.AssertNilDerefBranch builder helper; the test now covers the empty expression, constant non-nil fast path, and dynamic pointer nil-only branch, and verifies the emitted helper call, conditional branch, and unreachable panic block. Focused local coverage reports AssertNilDerefBranch at 100%.

@cpunion

cpunion commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Added focused fail-closed coverage in 1c5ec283c for the conservative non-nil proof boundaries: nil/visited values, pointer conversions, address equivalence, global-vs-local aliasing, distinct roots, memory side effects, and instructions without CFG ownership.

The previous Codecov report had 61 missed/partial lines in the new cl/instr.go regions; the focused coverage profile now hits 41 of those lines, in addition to the already-covered paths. Focused cl and ssa tests pass. A local full go test ./cl -coverprofile=... reached the package default 10m timeout late in TestRunAndTestFromTestrt/linkname; it was still progressing through integration cases, while CI uses the repository's 45m coverage timeout.

@cpunion

cpunion commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Codecov has now processed 1c5ec283c: patch coverage is 92.37% (351 hits, 20 misses, 9 partials), above the 88.21% target; overall reported coverage is 88.26%. The codecov/patch check is passing.

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