From 1f68584253ca080d183e6bb6a9daeda6c17fb8d2 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 16 Jul 2026 10:24:32 +0800 Subject: [PATCH 1/2] ssa: implement -B bounds-check disabling --- cmd/internal/compile/compile.go | 2 +- cmd/internal/compile/compile_test.go | 18 +- internal/build/bounds_checks_test.go | 185 ++++++++++++++++++ internal/build/build.go | 4 + internal/build/collect.go | 1 + internal/build/fingerprint.go | 29 +-- internal/build/fingerprint_test.go | 23 +++ internal/build/testdata/boundschecks/main.go | 74 +++++++ .../boundschecks_convert_short/main.go | 9 + .../testdata/boundschecks_required/main.go | 30 +++ ssa/datastruct.go | 49 ++++- ssa/expr.go | 12 +- ssa/package.go | 8 + test/goroot/xfail.yaml | 4 - 14 files changed, 415 insertions(+), 33 deletions(-) create mode 100644 internal/build/bounds_checks_test.go create mode 100644 internal/build/testdata/boundschecks/main.go create mode 100644 internal/build/testdata/boundschecks_convert_short/main.go create mode 100644 internal/build/testdata/boundschecks_required/main.go diff --git a/cmd/internal/compile/compile.go b/cmd/internal/compile/compile.go index 4699d81238..0e4cdab9e0 100644 --- a/cmd/internal/compile/compile.go +++ b/cmd/internal/compile/compile.go @@ -162,6 +162,7 @@ func runCmd(_ *base.Command, args []string) { conf.GoVersion = opts.lang conf.NoErrorColumn = opts.noColumns.value != 0 conf.AllowNoBody = !opts.complete + conf.DisableBoundsChecks = opts.noBounds.value != 0 var loaderCompilerFlags []string if opts.allErrors.value != 0 { loaderCompilerFlags = append(loaderCompilerFlags, "-e") @@ -195,7 +196,6 @@ func (opts *options) unsupported() []string { out = append(out, name) } } - appendFlag(opts.noBounds.value != 0, "-B") appendFlag(opts.dynlink, "-dynlink") appendFlag(opts.showOpt.value != 0, "-m") appendFlag(opts.live, "-live") diff --git a/cmd/internal/compile/compile_test.go b/cmd/internal/compile/compile_test.go index 9c135de888..344de66224 100644 --- a/cmd/internal/compile/compile_test.go +++ b/cmd/internal/compile/compile_test.go @@ -14,6 +14,7 @@ func TestGoCompilerFlagNamesAndTypes(t *testing.T) { opts := new(options) fs := newFlagSet(opts) err := fs.Parse([]string{ + "-B", "-c=2", "-C", "-e", @@ -27,7 +28,7 @@ func TestGoCompilerFlagNamesAndTypes(t *testing.T) { if err != nil { t.Fatal(err) } - if opts.concurrency != 2 || opts.noColumns.value != 1 || opts.allErrors.value != 1 || opts.noInline.value != 4 { + if opts.noBounds.value != 1 || opts.concurrency != 2 || opts.noColumns.value != 1 || opts.allErrors.value != 1 || opts.noInline.value != 4 { t.Fatalf("parsed flags: %+v", opts) } if opts.lang != "go1.17" || opts.pkgPath != "p" || opts.importCfg != "importcfg" { @@ -94,7 +95,6 @@ func TestCountAndListFlags(t *testing.T) { func TestUnsupportedCompilerFlags(t *testing.T) { opts := &options{ - noBounds: countFlag{value: 1}, dynlink: true, showOpt: countFlag{value: 1}, live: true, @@ -105,7 +105,7 @@ func TestUnsupportedCompilerFlags(t *testing.T) { writeBar: countFlag{set: true}, debug: stringListFlag{"panic,libfuzzer", "ssa/check/on,ssa/check/seed=1,wb"}, } - want := []string{"-B", "-dynlink", "-m", "-live", "-race", "-smallframes", "-std", "-+", "-wb", "-d=libfuzzer", "-d=wb"} + want := []string{"-dynlink", "-m", "-live", "-race", "-smallframes", "-std", "-+", "-wb", "-d=libfuzzer", "-d=wb"} if got := opts.unsupported(); !reflect.DeepEqual(got, want) { t.Fatalf("unsupported=%v, want %v", got, want) } @@ -120,7 +120,6 @@ func TestRunCmdValidationAndVersion(t *testing.T) { }{ {name: "bad flag", args: []string{"-unknown"}, wantCode: 2, wantOutput: "flag provided but not defined"}, {name: "no files", wantCode: 2, wantOutput: "no Go source files"}, - {name: "unsupported", args: []string{"-B", "case.go"}, wantCode: 2, wantOutput: "unsupported llgo compiler option(s): -B"}, {name: "negative concurrency", args: []string{"-c=-1", "case.go"}, wantCode: 2, wantOutput: "-c must be non-negative"}, {name: "invalid language", args: []string{"-lang=1.22", "case.go"}, wantCode: 2, wantOutput: "invalid value"}, {name: "version", args: []string{"-V"}, wantOutput: "compile version llgo"}, @@ -146,7 +145,7 @@ func TestRunCmdBuildsAndReportsErrors(t *testing.T) { } previousProcs := runtime.GOMAXPROCS(0) stdout, stderr, code := runCompileCommand(t, []string{ - "-c=1", "-C", "-e", "-lang=go1.22", "-N", "-l", "-complete", valid, + "-B", "-c=1", "-C", "-e", "-lang=go1.22", "-N", "-l", "-complete", valid, }) if code != 0 { t.Fatalf("valid compile exit code = %d; stdout=%q stderr=%q", code, stdout, stderr) @@ -155,6 +154,15 @@ func TestRunCmdBuildsAndReportsErrors(t *testing.T) { t.Fatalf("GOMAXPROCS = %d after compile, want restored value %d", got, previousProcs) } + zeroArray := dir + "/zero_array.go" + if err := os.WriteFile(zeroArray, []byte("package compilecase\ntype A [0]byte\nfunc Get(a *A, i int) byte { return a[i] }\n"), 0o644); err != nil { + t.Fatal(err) + } + stdout, stderr, code = runCompileCommand(t, []string{"-B", zeroArray}) + if code != 0 { + t.Fatalf("-B zero-length array compile exit code = %d; stdout=%q stderr=%q", code, stdout, stderr) + } + invalid := dir + "/invalid.go" if err := os.WriteFile(invalid, []byte("package compilecase\nvar _ = missing\n"), 0o644); err != nil { t.Fatal(err) diff --git a/internal/build/bounds_checks_test.go b/internal/build/bounds_checks_test.go new file mode 100644 index 0000000000..4d69408bc9 --- /dev/null +++ b/internal/build/bounds_checks_test.go @@ -0,0 +1,185 @@ +//go:build !llgo + +package build + +import ( + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" +) + +const boundsChecksFixture = "./testdata/boundschecks" + +func TestDisableBoundsChecksIR(t *testing.T) { + checked := boundsChecksModuleIR(t, false) + unchecked := boundsChecksModuleIR(t, true) + + for _, helper := range []string{"CheckIndexRange", "StringSlice2", "NewSlice2", "NewSlice3Bounds", "PanicSliceConvert"} { + if !strings.Contains(checked, helper) { + t.Errorf("default IR does not contain bounds helper %q", helper) + } + if strings.Contains(unchecked, helper) { + t.Errorf("-B IR unexpectedly contains bounds helper %q", helper) + } + } + + for _, function := range []string{"indexString", "indexSlice", "indexArray", "indexArrayPointer"} { + body := llvmFunctionBody(t, unchecked, function) + if strings.Contains(body, "CheckIndexRange") { + t.Errorf("-B %s contains an index bounds check:\n%s", function, body) + } + } + for _, function := range []string{"sliceString", "sliceSlice", "sliceArray", "sliceArrayPointer", "sliceThree"} { + body := llvmFunctionBody(t, unchecked, function) + if strings.Contains(body, "StringSlice2") || strings.Contains(body, "NewSlice2") || strings.Contains(body, "NewSlice3Bounds") { + t.Errorf("-B %s contains a slice bounds helper:\n%s", function, body) + } + } + for _, function := range []string{"shortSliceToArrayPointer", "shortSliceToArrayValue"} { + checkedBody := llvmFunctionBody(t, checked, function) + if !strings.Contains(checkedBody, "PanicSliceConvert") { + t.Errorf("default %s does not contain its conversion bounds check:\n%s", function, checkedBody) + } + uncheckedBody := llvmFunctionBody(t, unchecked, function) + if strings.Contains(uncheckedBody, "PanicSliceConvert") { + t.Errorf("-B %s contains a conversion bounds check:\n%s", function, uncheckedBody) + } + } + if body := llvmFunctionBody(t, unchecked, "shortSliceToArrayValue"); !strings.Contains(body, "load [4 x i8]") { + t.Errorf("slice-to-array value conversion does not dereference its converted pointer:\n%s", body) + } + + for _, function := range []string{"indexArrayPointer", "sliceArrayPointer"} { + body := llvmFunctionBody(t, unchecked, function) + if !strings.Contains(body, "AssertNilDeref") { + t.Errorf("-B %s lost its *array nil check:\n%s", function, body) + } + } + for _, width := range []string{"zext i8", "zext i16", "zext i32"} { + if !strings.Contains(unchecked, width) { + t.Errorf("-B IR does not retain integer-width conversion %q", width) + } + } + for _, function := range []string{"makeUnsafeString", "makeUnsafeSlice"} { + body := llvmFunctionBody(t, unchecked, function) + if !strings.Contains(body, "AssertRuntimeError") { + t.Errorf("-B %s lost mandatory unsafe builtin checks:\n%s", function, body) + } + } +} + +func TestDisableBoundsChecksLegalResultsMatchDefault(t *testing.T) { + wantFields := []string{"98", "30", "10", "40", "bc", "2", "3", "2", "3", "2", "3", "2", "3", "10", "40", "20", "30"} + checked := runBinary(t, buildBoundsChecksBinary(t, false)) + unchecked := runBinary(t, buildBoundsChecksBinary(t, true)) + if checked != unchecked { + t.Fatalf("default and -B output differ:\ndefault %q\n-B %q", checked, unchecked) + } + if fields := strings.Fields(unchecked); !reflect.DeepEqual(fields, wantFields) { + t.Fatalf("legal -B results = %v, want %v", fields, wantFields) + } +} + +func TestDisableBoundsChecksShortSliceConversionsDoNotPanic(t *testing.T) { + path := filepath.Join(t.TempDir(), "bounds-disabled") + if runtime.GOOS == "windows" { + path += ".exe" + } + conf := NewDefaultConf(ModeBuild) + conf.OutFile = path + conf.DisableBoundsChecks = true + if _, err := Do([]string{"./testdata/boundschecks_convert_short"}, conf); err != nil { + t.Fatalf("build short conversion fixture with bounds checks disabled: %v", err) + } + if output := runBinary(t, path); !reflect.DeepEqual(strings.Fields(output), []string{"1", "4", "1", "4"}) { + fields := strings.Fields(output) + t.Fatalf("short conversions with bounds checks disabled = %v, want [1 4 1 4]; output %q", fields, output) + } +} + +func TestDisableBoundsChecksRetainsRequiredPanics(t *testing.T) { + output := runBinary(t, buildBoundsChecksBinaryFrom(t, "./testdata/boundschecks_required", true)) + want := []string{"true", "true", "true", "true"} + if fields := strings.Fields(output); !reflect.DeepEqual(fields, want) { + t.Fatalf("required -B panics = %v, want %v; output %q", fields, want, output) + } +} + +func boundsChecksModuleIR(t *testing.T, disable bool) string { + t.Helper() + conf := NewDefaultConf(ModeGen) + conf.DisableBoundsChecks = disable + var ir string + conf.ModuleHook = func(pkg Package) { + module := pkg.LPkg.String() + if strings.Contains(module, ".indexString\"") { + ir = module + } + } + pkgs, err := Do([]string{boundsChecksFixture}, conf) + if err != nil { + t.Fatalf("generate bounds-check IR (disabled=%v): %v", disable, err) + } + if len(pkgs) != 1 || pkgs[0].LPkg == nil { + t.Fatalf("generate bounds-check IR (disabled=%v): packages = %#v", disable, pkgs) + } + defer pkgs[0].LPkg.Prog.Dispose() + if ir == "" { + t.Fatalf("generate bounds-check IR (disabled=%v): fixture module was not observed", disable) + } + return ir +} + +func buildBoundsChecksBinary(t *testing.T, disable bool) string { + t.Helper() + return buildBoundsChecksBinaryFrom(t, boundsChecksFixture, disable) +} + +func buildBoundsChecksBinaryFrom(t *testing.T, fixture string, disable bool) string { + t.Helper() + name := "checked" + if disable { + name = "unchecked" + } + if runtime.GOOS == "windows" { + name += ".exe" + } + path := filepath.Join(t.TempDir(), name) + conf := NewDefaultConf(ModeBuild) + conf.OutFile = path + conf.DisableBoundsChecks = disable + if _, err := Do([]string{fixture}, conf); err != nil { + t.Fatalf("build bounds-check fixture (disabled=%v): %v", disable, err) + } + return path +} + +func llvmFunctionBody(t *testing.T, module, name string) string { + t.Helper() + marker := "." + name + "\"(" + markerAt := 0 + start := -1 + for { + next := strings.Index(module[markerAt:], marker) + if next < 0 { + break + } + markerAt += next + lineStart := strings.LastIndex(module[:markerAt], "\n") + 1 + if strings.HasPrefix(module[lineStart:markerAt], "define ") { + start = lineStart + break + } + markerAt += len(marker) + } + if start < 0 { + t.Fatalf("LLVM definition for %q not found", name) + } + end := strings.Index(module[markerAt:], "\n}") + if end < 0 { + t.Fatalf("end of LLVM definition for %q not found", name) + } + return module[start : markerAt+end+2] +} diff --git a/internal/build/build.go b/internal/build/build.go index 2de1f96536..237f744905 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -177,6 +177,9 @@ type Config struct { // default. PCLNModeSet bool AllowNoBody bool // allow declarations without bodies, as go tool compile does + // DisableBoundsChecks disables index, slice, and slice-to-array conversion + // bounds checks while retaining required integer conversions and nil checks. + DisableBoundsChecks bool // PthreadStackSize sets a custom stack size, in bytes, for pthread-backed // goroutines. A zero value keeps the platform pthread default. @@ -365,6 +368,7 @@ func Do(args []string, conf *Config) ([]Package, error) { } prog := llssa.NewProgram(target) + prog.DisableBoundsChecks(conf.DisableBoundsChecks) if conf.Mode != ModeGen { // ModeGen callers (llgen and the golden suites) read LPkg.String() // after Do returns and dispose the program themselves; every other diff --git a/internal/build/collect.go b/internal/build/collect.go index f482762fb7..a44f97f73c 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -111,6 +111,7 @@ func (c *context) collectCommonInputs(m *manifestBuilder) { m.common.GoGlobalDCE = c.buildConf.goGlobalDCEEnabled() m.common.EmitDWARF = shouldEmitDebugInfo(c.buildConf, &c.crossCompile) m.common.PCLNMode = effectivePCLNMode(c.buildConf).String() + m.common.DisableBoundsChecks = c.buildConf.DisableBoundsChecks // Compiler configuration if c.crossCompile.CC != "" { diff --git a/internal/build/fingerprint.go b/internal/build/fingerprint.go index aba29f23c1..121526bcff 100644 --- a/internal/build/fingerprint.go +++ b/internal/build/fingerprint.go @@ -112,24 +112,25 @@ func (s *envSection) empty() bool { } type commonSection struct { - AbiMode string `yaml:"ABI_MODE,omitempty"` - BuildTags []string `yaml:"BUILD_TAGS,omitempty"` - Target string `yaml:"TARGET,omitempty"` - TargetABI string `yaml:"TARGET_ABI,omitempty"` - GoGlobalDCE bool `yaml:"GO_GLOBAL_DCE,omitempty"` - EmitDWARF bool `yaml:"EMIT_DWARF,omitempty"` - PCLNMode string `yaml:"PCLN_MODE,omitempty"` - CC string `yaml:"CC,omitempty"` - CCFlags []string `yaml:"CCFLAGS,omitempty"` - CFlags []string `yaml:"CFLAGS,omitempty"` - LDFlags []string `yaml:"LDFLAGS,omitempty"` - Linker string `yaml:"LINKER,omitempty"` - ExtraFiles []fileDigest `yaml:"EXTRA_FILES,omitempty"` + AbiMode string `yaml:"ABI_MODE,omitempty"` + BuildTags []string `yaml:"BUILD_TAGS,omitempty"` + Target string `yaml:"TARGET,omitempty"` + TargetABI string `yaml:"TARGET_ABI,omitempty"` + GoGlobalDCE bool `yaml:"GO_GLOBAL_DCE,omitempty"` + EmitDWARF bool `yaml:"EMIT_DWARF,omitempty"` + PCLNMode string `yaml:"PCLN_MODE,omitempty"` + DisableBoundsChecks bool `yaml:"DISABLE_BOUNDS_CHECKS,omitempty"` + CC string `yaml:"CC,omitempty"` + CCFlags []string `yaml:"CCFLAGS,omitempty"` + CFlags []string `yaml:"CFLAGS,omitempty"` + LDFlags []string `yaml:"LDFLAGS,omitempty"` + Linker string `yaml:"LINKER,omitempty"` + ExtraFiles []fileDigest `yaml:"EXTRA_FILES,omitempty"` } func (s *commonSection) empty() bool { return s.AbiMode == "" && len(s.BuildTags) == 0 && s.Target == "" && s.TargetABI == "" && - !s.GoGlobalDCE && !s.EmitDWARF && s.PCLNMode == "" && s.CC == "" && len(s.CCFlags) == 0 && len(s.CFlags) == 0 && len(s.LDFlags) == 0 && + !s.GoGlobalDCE && !s.EmitDWARF && s.PCLNMode == "" && !s.DisableBoundsChecks && s.CC == "" && len(s.CCFlags) == 0 && len(s.CFlags) == 0 && len(s.LDFlags) == 0 && s.Linker == "" && len(s.ExtraFiles) == 0 } diff --git a/internal/build/fingerprint_test.go b/internal/build/fingerprint_test.go index 54bba13694..697f9ef8af 100644 --- a/internal/build/fingerprint_test.go +++ b/internal/build/fingerprint_test.go @@ -131,6 +131,29 @@ func TestManifestBuilder_FingerprintDifferentValues(t *testing.T) { } } +func TestManifestBuilder_DisableBoundsChecks(t *testing.T) { + checked := newManifestBuilder() + unchecked := newManifestBuilder() + unchecked.common.DisableBoundsChecks = true + + if !checked.common.empty() { + t.Fatal("default common section is not empty") + } + if unchecked.common.empty() { + t.Fatal("disabled bounds checks did not make the common section non-empty") + } + if checked.Fingerprint() == unchecked.Fingerprint() { + t.Fatal("disabled bounds checks did not change the build fingerprint") + } + data, err := decodeManifest(unchecked.Build()) + if err != nil { + t.Fatalf("decodeManifest: %v", err) + } + if data.Common == nil || !data.Common.DisableBoundsChecks { + t.Fatalf("decoded common section = %#v, want disabled bounds checks", data.Common) + } +} + func TestManifestBuilder_EmptySections(t *testing.T) { m := newManifestBuilder() content := m.Build() diff --git a/internal/build/testdata/boundschecks/main.go b/internal/build/testdata/boundschecks/main.go new file mode 100644 index 0000000000..97d04ede99 --- /dev/null +++ b/internal/build/testdata/boundschecks/main.go @@ -0,0 +1,74 @@ +package main + +import "unsafe" + +func indexString(v string, i uint8) byte { + return v[i] +} + +func indexSlice(v []byte, i uint8) byte { + return v[i] +} + +func indexArray(v [4]byte, i uint8) byte { + return v[i] +} + +func indexArrayPointer(v *[4]byte, i uint8) byte { + return v[i] +} + +func sliceString(v string, low uint8, high uint16) string { + return v[low:high] +} + +func sliceSlice(v []byte, low uint8, high uint16) []byte { + return v[low:high] +} + +func sliceArray(v [4]byte, low uint8, high uint16) []byte { + return v[low:high] +} + +func sliceArrayPointer(v *[4]byte, low uint8, high uint16) []byte { + return v[low:high] +} + +func sliceThree(v []byte, low uint8, high uint16, max uint32) []byte { + return v[low:high:max] +} + +func shortSliceToArrayPointer(v []byte) *[4]byte { + return (*[4]byte)(v) +} + +func shortSliceToArrayValue(v []byte) [4]byte { + return [4]byte(v) +} + +func makeUnsafeString(v *byte, n int) string { + return unsafe.String(v, n) +} + +func makeUnsafeSlice(v *byte, n int) []byte { + return unsafe.Slice(v, n) +} + +func main() { + str := "abcd" + slice := []byte{10, 20, 30, 40} + array := [4]byte{10, 20, 30, 40} + arrayPointer := &array + + println(indexString(str, 1), indexSlice(slice, 2), indexArray(array, 0), indexArrayPointer(arrayPointer, 3)) + println( + sliceString(str, 1, 3), + len(sliceSlice(slice, 1, 3)), cap(sliceSlice(slice, 1, 3)), + len(sliceArray(array, 1, 3)), cap(sliceArray(array, 1, 3)), + len(sliceArrayPointer(arrayPointer, 1, 3)), cap(sliceArrayPointer(arrayPointer, 1, 3)), + len(sliceThree(slice, 1, 3, 4)), cap(sliceThree(slice, 1, 3, 4)), + ) + arrayFromPointer := shortSliceToArrayPointer(slice) + arrayValue := shortSliceToArrayValue(slice) + println(arrayFromPointer[0], arrayFromPointer[3], arrayValue[1], arrayValue[2]) +} diff --git a/internal/build/testdata/boundschecks_convert_short/main.go b/internal/build/testdata/boundschecks_convert_short/main.go new file mode 100644 index 0000000000..c86f51bffa --- /dev/null +++ b/internal/build/testdata/boundschecks_convert_short/main.go @@ -0,0 +1,9 @@ +package main + +func main() { + backing := [4]byte{1, 2, 3, 4} + short := backing[:1] + ptr := (*[4]byte)(short) + value := [4]byte(short) + println(ptr[0], ptr[3], value[0], value[3]) +} diff --git a/internal/build/testdata/boundschecks_required/main.go b/internal/build/testdata/boundschecks_required/main.go new file mode 100644 index 0000000000..a245108e77 --- /dev/null +++ b/internal/build/testdata/boundschecks_required/main.go @@ -0,0 +1,30 @@ +package main + +import "unsafe" + +func didPanic(f func()) (panicked bool) { + defer func() { + panicked = recover() != nil + }() + f() + return +} + +func main() { + println( + didPanic(func() { + var array *[4]byte + _ = array[0] + }), + didPanic(func() { + var array *[4]byte + _ = array[:] + }), + didPanic(func() { + _ = unsafe.String((*byte)(nil), 1) + }), + didPanic(func() { + _ = unsafe.Slice((*byte)(nil), 1) + }), + ) +} diff --git a/ssa/datastruct.go b/ssa/datastruct.go index a81032a177..148739bc2c 100644 --- a/ssa/datastruct.go +++ b/ssa/datastruct.go @@ -245,8 +245,10 @@ func (b Builder) boundsArg(idx Expr) (Expr, bool) { // check index >= 0 && index < max and size to uint func (b Builder) checkIndex(idx Expr, max Expr) Expr { prog := b.Prog - // check range - checkMin, checkMax := checkRange(idx, max) + var checkMin, checkMax bool + if !prog.disableBoundsChecks { + checkMin, checkMax = checkRange(idx, max) + } // fit size signed := idx.kind == vkSigned var typ Type @@ -260,6 +262,9 @@ func (b Builder) checkIndex(idx Expr, max Expr) Expr { idx.Type = typ idx.impl = castUintptr(b, idx.impl, srcType, typ) } + if prog.disableBoundsChecks { + return idx + } // check range expr var check Expr if checkMin { @@ -378,7 +383,11 @@ func (b Builder) Slice(x, low, high, max Expr) (ret Expr) { highArg, highSigned = b.boundsArg(high) } ret.Type = x.Type - ret.impl = b.InlineCall(b.Pkg.rtFunc("StringSlice2"), x, lowArg, highArg, prog.BoolVal(lowSigned), prog.BoolVal(highSigned)).impl + if prog.disableBoundsChecks { + ret.impl = b.stringSliceUnchecked(x, low, high).impl + } else { + ret.impl = b.InlineCall(b.Pkg.rtFunc("StringSlice2"), x, lowArg, highArg, prog.BoolVal(lowSigned), prog.BoolVal(highSigned)).impl + } return case *types.Slice: nEltSize = SizeOf(prog, prog.Index(x.Type)) @@ -399,7 +408,7 @@ func (b Builder) Slice(x, low, high, max Expr) (ret Expr) { nCap = prog.IntVal(uint64(te.Len()), prog.Int()) upperIsLen = true if high.IsNil() { - if lowIsNil && max.IsNil() { + if lowIsNil && max.IsNil() && !prog.disableBoundsChecks { ret.impl = b.unsafeSlice(x, nCap.impl, nCap.impl).impl return } @@ -409,6 +418,17 @@ func (b Builder) Slice(x, low, high, max Expr) (ret Expr) { base = x } } + if prog.disableBoundsChecks { + if _, ok := x.raw.Type.Underlying().(*types.Pointer); ok && !isKnownNonNilArrayBase(x.impl) { + b.AssertNilDeref(x) + } + upper := nCap + if !max.IsNil() { + upper = max + } + ret.impl = b.sliceUnchecked(ret.Type, base, low, high, upper).impl + return + } if max.IsNil() { ret.impl = b.InlineCall( b.Pkg.rtFunc("NewSlice2"), @@ -439,6 +459,27 @@ func (b Builder) Slice(x, low, high, max Expr) (ret Expr) { return } +func (b Builder) stringSliceUnchecked(x, low, high Expr) Expr { + data := b.StringData(x) + advanced := b.Advance(data, low) + beforeEnd := llvm.CreateICmp(b.impl, llvm.IntSLT, low.impl, b.StringLen(x).impl) + data.impl = llvm.CreateSelect(b.impl, beforeEnd, advanced.impl, data.impl) + length := b.impl.CreateSub(high.impl, low.impl, "") + return b.unsafeString(data.impl, length) +} + +func (b Builder) sliceUnchecked(t Type, base, low, high, upper Expr) Expr { + length := b.impl.CreateSub(high.impl, low.impl, "") + capacity := b.impl.CreateSub(upper.impl, low.impl, "") + advanced := b.Advance(base, low) + zero := llvm.ConstInt(capacity.Type(), 0, false) + hasCapacity := llvm.CreateICmp(b.impl, llvm.IntSGT, capacity, zero) + base.impl = llvm.CreateSelect(b.impl, hasCapacity, advanced.impl, base.impl) + ret := b.unsafeSlice(base, length, capacity) + ret.Type = t + return ret +} + // SliceLit creates a new slice with the specified elements. func (b Builder) SliceLit(t Type, elts ...Expr) Expr { prog := b.Prog diff --git a/ssa/expr.go b/ssa/expr.go index caee097c06..c56c5f09e3 100644 --- a/ssa/expr.go +++ b/ssa/expr.go @@ -1455,11 +1455,13 @@ func (b Builder) SelectValue(cond Expr, a Expr, bExpr Expr) Expr { // t1 = slice to array pointer *[4]byte <- []byte (t0) func (b Builder) SliceToArrayPointer(x Expr, typ Type) (ret Expr) { ret.Type = typ - max := b.Prog.IntVal(uint64(typ.RawType().Underlying().(*types.Pointer).Elem().Underlying().(*types.Array).Len()), b.Prog.Int()) - failed := Expr{llvm.CreateICmp(b.impl, llvm.IntSLT, b.SliceLen(x).impl, max.impl), b.Prog.Bool()} - b.IfThen(failed, func() { - b.InlineCall(b.Pkg.rtFunc("PanicSliceConvert"), max, b.SliceLen(x)) - }) + if !b.Prog.disableBoundsChecks { + max := b.Prog.IntVal(uint64(typ.RawType().Underlying().(*types.Pointer).Elem().Underlying().(*types.Array).Len()), b.Prog.Int()) + failed := Expr{llvm.CreateICmp(b.impl, llvm.IntSLT, b.SliceLen(x).impl, max.impl), b.Prog.Bool()} + b.IfThen(failed, func() { + b.InlineCall(b.Pkg.rtFunc("PanicSliceConvert"), max, b.SliceLen(x)) + }) + } ret.impl = b.SliceData(x).impl return } diff --git a/ssa/package.go b/ssa/package.go index a13ccae93b..13936a8fa2 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -235,6 +235,7 @@ type aProgram struct { disposed bool enableGoGlobalDCE bool + disableBoundsChecks bool pthreadStackSize uint64 enableLTOPluginMarker bool @@ -352,6 +353,13 @@ func (p Program) EnableGoGlobalDCE(enable bool) { p.enableGoGlobalDCE = enable } +// DisableBoundsChecks controls index, slice, and slice-to-array conversion +// bounds checks. Other dynamic validity checks, including nil pointer and +// unsafe builtin checks, are not affected. +func (p Program) DisableBoundsChecks(disable bool) { + p.disableBoundsChecks = disable +} + func (p Program) SetPthreadStackSize(size uint64) { p.pthreadStackSize = size } diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index 2f30a6997f..8b91470b21 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2345,10 +2345,6 @@ xfails: directive: errorcheck case: syntax/vareq.go reason: llgo parser recovery emits additional diagnostics after the expected syntax error - - version: go1.26 - directive: compile - case: fixedbugs/issue48092.go - reason: gc-specific -B bounds-checking backend option is not implemented by llgo - version: go1.26 directive: compile case: typeparam/issue50993.go From 05a66639398d7bc9a6ce210ad392a90224d80af3 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 16 Jul 2026 19:04:05 +0800 Subject: [PATCH 2/2] test: cover disabled bounds-check lowering --- ssa/bounds_checks_test.go | 80 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 ssa/bounds_checks_test.go diff --git a/ssa/bounds_checks_test.go b/ssa/bounds_checks_test.go new file mode 100644 index 0000000000..3233824d10 --- /dev/null +++ b/ssa/bounds_checks_test.go @@ -0,0 +1,80 @@ +//go:build !llgo + +package ssa_test + +import ( + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/ssa" + "github.com/goplus/llgo/ssa/ssatest" +) + +func TestBoundsCheckModesIR(t *testing.T) { + checked := boundsCheckModeIR(t, false) + unchecked := boundsCheckModeIR(t, true) + + for _, helper := range []string{ + "CheckIndexRange", + "StringSlice2", + "NewSlice2", + "NewSlice3Bounds", + "PanicSliceConvert", + } { + if !strings.Contains(checked, helper) { + t.Errorf("checked IR does not contain %q", helper) + } + if strings.Contains(unchecked, helper) { + t.Errorf("unchecked IR contains %q", helper) + } + } + if !strings.Contains(unchecked, "AssertNilDeref") { + t.Error("unchecked *array slice lost its nil check") + } + if got := strings.Count(unchecked, "select i1"); got < 4 { + t.Errorf("unchecked IR contains %d select operations, want at least 4", got) + } +} + +func boundsCheckModeIR(t *testing.T, disable bool) string { + t.Helper() + prog := ssatest.NewProgram(t, nil) + t.Cleanup(prog.Dispose) + prog.DisableBoundsChecks(disable) + + byteSlice := types.NewSlice(types.Typ[types.Byte]) + byteArray := types.NewArray(types.Typ[types.Byte], 4) + byteArrayPtr := types.NewPointer(byteArray) + params := types.NewTuple( + types.NewVar(0, nil, "str", types.Typ[types.String]), + types.NewVar(0, nil, "slice", byteSlice), + types.NewVar(0, nil, "array", byteArrayPtr), + types.NewVar(0, nil, "low", types.Typ[types.Int]), + types.NewVar(0, nil, "high", types.Typ[types.Int]), + types.NewVar(0, nil, "max", types.Typ[types.Int]), + ) + sig := types.NewSignatureType(nil, nil, nil, params, nil, false) + pkg := prog.NewPackage("bounds", "example.com/bounds") + fn := pkg.NewFunc("modes", sig, ssa.InGo) + b := fn.MakeBody(1) + + str := fn.Param(0) + slice := fn.Param(1) + array := fn.Param(2) + low := fn.Param(3) + high := fn.Param(4) + max := fn.Param(5) + none := ssa.Expr{} + + b.Index(str, low, nil) + b.IndexAddr(slice, low) + b.Slice(str, low, high, none) + b.Slice(slice, low, high, none) + b.Slice(slice, low, high, max) + b.Slice(array, none, none, none) + b.SliceToArrayPointer(slice, prog.Type(byteArrayPtr, ssa.InGo)) + b.Return() + b.EndBuild() + return pkg.String() +}