Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/internal/compile/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
18 changes: 13 additions & 5 deletions cmd/internal/compile/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func TestGoCompilerFlagNamesAndTypes(t *testing.T) {
opts := new(options)
fs := newFlagSet(opts)
err := fs.Parse([]string{
"-B",
"-c=2",
"-C",
"-e",
Expand All @@ -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" {
Expand Down Expand Up @@ -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,
Expand All @@ -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)
}
Expand All @@ -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"},
Expand All @@ -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)
Expand All @@ -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)
Expand Down
185 changes: 185 additions & 0 deletions internal/build/bounds_checks_test.go
Original file line number Diff line number Diff line change
@@ -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]
}
4 changes: 4 additions & 0 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions internal/build/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
29 changes: 15 additions & 14 deletions internal/build/fingerprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
23 changes: 23 additions & 0 deletions internal/build/fingerprint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading
Loading