Skip to content

ssa: use ptrtoaddr for unsafe builtin bounds checks - #2515

Open
zhouguangyuan0718 wants to merge 1 commit into
xgo-dev:mainfrom
zhouguangyuan0718:codex/llvm22-unsafe-ptrtoaddr
Open

zhouguangyuan0718 wants to merge 1 commit into
xgo-dev:mainfrom
zhouguangyuan0718:codex/llvm22-unsafe-ptrtoaddr

Conversation

@zhouguangyuan0718

Copy link
Copy Markdown
Collaborator

unsafe.String and unsafe.Slice bounds checks only need the input pointer's address to detect wraparound, but currently emit ptrtoint, which also exposes pointer provenance. Use LLVM 22 ptrtoaddr for this check while retaining the original pointer in the resulting string or slice.

The maximum byte length and wraparound arithmetic use the DataLayout address width. For a 64-bit pointer representation with a 32-bit index width, the check rejects oversized lengths before truncating the offset and performs the end-address addition in 32 bits. General unsafe.Pointer/uintptr conversions, map keys, and FFI paths keep their existing behavior. The DCE override constant cloner also preserves ptrtoaddr constant expressions.

Depends on xgo-dev/llvm#53. The module replacement temporarily pins its exact personal-fork commit 18183e7fb5bf4204465c04ab41ec8682caf47631; replace it with the upstream binding release after that PR lands.

Validation on macOS arm64 with LLVM and LLD 22.1.8:

  • go test ./ssa ./internal/dcepass -count=1, including new 32-bit, 64-bit, and mixed pointer/index-width IR regressions and constant-cloning coverage.
  • Rebuilt the LLGo CLI against the remote binding pin and ran llgo test ./test/go ./test/std/unsafe -run 'TestUnsafe|TestSlice|TestString' -count=1. Existing unsafe builtins tests cover negative/oversized lengths, nil pointers, element-size overflow, end-address wraparound, and the last-byte boundary.
  • go mod verify and git diff --check.

Cross-platform CI is pending; no end-to-end performance claim is made.

@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: switch unsafe bounds check to ptrtoaddr

The core change in ssa/expr.go is well-reasoned and correct. Using ptrtoaddr with the DataLayout address width instead of ptrtoint with the pointer size is the right approach for a provenance-free address observation, and the cross-architecture test coverage (amd64/386/custom datalayout) is thorough.

I specifically verified the main correctness concern — that truncating lastOffset from pointer width to the (possibly narrower) address width could hide an overflow — and it does not materialize. The lenTooLarge guard runs and traps first: any length reaching the truncation satisfies byteSize = length*elemSize <= maxAddr, so lastOffset = byteSize-1 fits losslessly in the address width. The length == 0 case is correctly masked by the isNonZero gate on the final assert. The overflow detection is sound, and for narrow-address targets it is more correct than the old pointer-width approach.

The main blocking concern is the personal-fork replace directive; two minor readability notes are inline.

Comment thread go.mod

replace github.com/xgo-dev/llgo/runtime => ./runtime

replace github.com/xgo-dev/llvm => github.com/zhouguangyuan0718/go-llvm v0.0.0-20260906104504-18183e7fb5bf

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.

[P1] Fork replace directive should not merge to main

This points the LLVM binding at a personal fork pinned to a pseudo-version. Since ptrtoaddr/ConstPtrToAddr only exist in that fork, merging the source changes without publishing the binding upstream breaks the build for anyone not using the fork, and ties the project to a personal repo that may disappear or diverge (supply-chain surface). Recommendation: land the ptrtoaddr/ConstPtrToAddr bindings in github.com/xgo-dev/llvm, bump the require to a tagged release, and drop this replace (and the corresponding go.sum entries) before merge. Also note ptrtoaddr requires LLVM 21+; consider documenting the minimum LLVM version.

Comment thread ssa/expr.go
}
lastOffset := b.impl.CreateSub(byteSize, llvm.ConstInt(uptr.ll, 1, false), "")
addr := llvm.CreatePtrToInt(b.impl, data.impl, uptr.ll)
if lastOffset.Type() != addrTy {

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.

[P3] Document the invariant that makes the trunc lossless

The truncation of lastOffset to the address width is provably safe, but the invariant that guarantees it — byteSize <= maxAddr, enforced by the lenTooLarge check above — is implicit. A one-line comment (e.g. "byteSize <= maxAddr per the len check above, so the trunc is lossless") would spare future readers from worrying that high bits are silently dropped.

Comment thread ssa/unsafe_bounds_test.go
b.Return()
b.EndBuild()
ir := fn.impl.String()
if !strings.Contains(ir, fmt.Sprintf("ptrtoaddr ptr %%p to i%d", tc.width)) || strings.Contains(ir, "ptrtoint") {

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.

[P3] Blanket ptrtoint assertion is broader than its message

The negative check rejects any ptrtoint anywhere in the function IR, while the failure message speaks only about pointer-provenance of the address observation. This passes today, but if unrelated future codegen in this path ever emits a ptrtoint, the test would fail with a misleading provenance message. Consider scoping the check to the operand (e.g. ptrtoint ptr %p) to match the stated intent. Not a current bug.

@codecov

codecov Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
ssa/expr.go 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

LLGo WebAssembly build benchmarks

6e1b0d264059 | workflow run | long-term charts

WebAssembly output sizes

Profile and compiler Wasm module vs base Generated JS glue vs base
ec32/LLGo 112737 B 0 B / +0.0% 70724 B 0 B / +0.0%
ec64/LLGo 118288 B 0 B / +0.0% 74021 B 0 B / +0.0%
js/Go 1895533 B 0 B / +0.0% 0 B 0 B / 0.0%
js/LLGo 65512 B 0 B / +0.0% 68499 B 0 B / +0.0%
wasip1/Go 1909947 B 0 B / +0.0% 0 B 0 B / 0.0%
wasip1/LLGo 71802 B 0 B / +0.0% 0 B 0 B / 0.0%
wc32/LLGo 116701 B 0 B / +0.0% 0 B 0 B / 0.0%

LLGo WebAssembly build measurements

Profile Build vs base
ec32 5.390 s +50.48 ms / +0.9% (worse)
ec64 5.285 s +418.9 ms / +8.6% (worse)
js 4.407 s +122 ms / +2.8% (worse)
wasip1 3.241 s +349.1 ms / +12.1% (worse)
wc32 3.853 s +117.9 ms / +3.2% (worse)

Compared with 9317592bb30a measured in the same runner job.

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