Skip to content
Merged
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
92 changes: 92 additions & 0 deletions cl/debug_zero_size_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package cl

import (
"go/ast"
"go/parser"
"go/token"
"go/types"
"testing"

"github.com/xgo-dev/llgo/internal/optlevel"
llssa "github.com/xgo-dev/llgo/ssa"
"github.com/xgo-dev/llvm"
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/ssa/ssautil"
)

// GOROOT/test/abi/f_ret_z_not.go crashed in LLVM 22's
// SelectionDAGBuilder::resolveDanglingDebugInfo after optimization promoted an
// empty aggregate snapshot to dbg.value(empty zeroinitializer). Module
// verification alone does not catch it: exercise object emission too.
func TestZeroSizedDebugValuesEmitObjects(t *testing.T) {
const source = `package zero
type Z struct{}
type NZ struct { x, y int }
//go:noinline
func f(x, y int) (Z, NZ, Z) {
var z Z
return z, NZ{x, y}, z
}
//go:noinline
func g() (Z, NZ, Z) {
a, b, c := f(3, 4)
return c, b, a
}
func array(z [0]int) ([0]int, int, [0]int) {
var empty [0]int
return empty, 7, z
}
func check() int {
_, b, _ := g()
return b.x + b.y
}
`
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "zero.go", source, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
ssaPkg, _, err := ssautil.BuildPackage(&types.Config{}, fset,
types.NewPackage("zero", "zero"), []*ast.File{file},
ssa.SanityCheckFunctions|ssa.InstantiateGenerics|ssa.GlobalDebug)
if err != nil {
t.Fatal(err)
}
for _, target := range []struct{ name, goos, triple string }{
{"darwin", "darwin", "aarch64-apple-darwin"},
{"linux", "linux", "aarch64-unknown-linux-gnu"},
{"windows-msvc", "windows", "aarch64-pc-windows-msvc"},
{"windows-mingw", "windows", "aarch64-w64-windows-gnu"},
} {
for _, level := range []optlevel.Level{optlevel.O0, optlevel.O2, optlevel.Os} {
t.Run(target.name+"/"+level.String(), func(t *testing.T) {
prog := newLLSSAProgForTarget(t, &llssa.Target{
GOOS: target.goos, GOARCH: "arm64", LLVMTarget: target.triple, OptLevel: level,
})
defer prog.Dispose()
pkg, _, err := newPackageEx(prog, nil, nil, nil, ssaPkg,
[]*ast.File{file}, nil, false, Options{Debug: true, DebugSymbols: true})
if err != nil {
t.Fatal(err)
}
mod := pkg.Module()
mod.SetDataLayout(prog.DataLayout())
mod.SetTarget(target.triple)
options := llvm.NewPassBuilderOptions()
defer options.Dispose()
options.SetVerifyEach(true)
if err := mod.RunPasses("default<"+level.String()+">", prog.TargetMachine(), options); err != nil {
t.Fatal(err)
}
object, err := prog.TargetMachine().EmitToMemoryBuffer(mod, llvm.ObjectFile)
if err != nil {
t.Fatal(err)
}
defer object.Dispose()
if len(object.Bytes()) == 0 {
t.Fatal("empty object")
}
})
}
}
}
13 changes: 12 additions & 1 deletion ssa/di.go
Original file line number Diff line number Diff line change
Expand Up @@ -908,15 +908,26 @@ func (b Builder) doConstructDebugAddrWithStore(v Expr, t types.Type) (dbgPtr, st
default:
ty = v.Type
}
// Empty aggregates have no SelectionDAG value. LLVM 22 can crash when
// promoting their debug snapshots to dbg.value(empty zeroinitializer).
// Back the debug-only address with a byte while keeping the source type
// in the variable metadata; there are no source value bits to copy.
storedValue := v
if b.Prog.SizeOf(ty) == 0 {
ty = b.Prog.Byte()
storedValue = b.Prog.IntVal(0, ty)
}
// A debug snapshot reserves one slot per function invocation. Allocating
// it at a DebugRef inside a loop grows the stack on every iteration at O0.
// Keep the value update at its source position, but reserve the slot with
// the same entry-block builder used for ordinary stack locals.
entryBuilder := *b
entryBuilder.impl = b.Func.entryAllocaBuilder()
dbgPtr = entryBuilder.AllocaT(ty)
// Store through the allocated type so closure snapshots also initialize
// their data field before exposing the source-typed debug address.
store = b.Store(dbgPtr, storedValue)

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.

Minor / non-blocking: the Store is now intentionally emitted while dbgPtr.Type is still Pointer(ty) (the alloca'd type), before it's reset to Pointer(v.Type). For most types where ty != v.Type (slice, string, named) this is benign — the same store is emitted. For signature/closure values it's a real emitted-IR change: Store now takes the closure branch and writes a full {funcptr, nil} aggregate, whereas the old order stored only the function pointer and left the $data slot as uninitialized stack garbage. This is actually an improvement (debug-only), but the surrounding comment only explains the zero-sized rationale. Consider a one-line note that the Store deliberately runs under the alloca type ty so closure-typed debug values are fully initialized.

dbgPtr.Type = b.Prog.Pointer(v.Type)
store = b.Store(dbgPtr, v)
return dbgPtr, store
}

Expand Down
3 changes: 3 additions & 0 deletions test/goroot/notapplicable.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
# under test and explains the LLGo design choice that puts it out of scope.
# Applicability is global: entries must not select a Go version or platform.
not_applicable:
- directive: run
case: fixedbugs/issue75764.go
reason: "not applicable: this case asserts cmd/compile's promoted-interface-wrapper tail-call optimization through a wall-clock ratio for 1,000 versus 10,000 wrappers; LLGo delegates optimization to LLVM and does not require gc's constant-time wrapper-chain performance, so this compiler-specific optimization benchmark is outside language compatibility goals"
- directive: run
case: finprofiled.go
reason: "not applicable: this case requires gc's 16-byte tiny allocator blocks and private MemProfile bookkeeping for finalizer-resurrected subobjects; LLGo uses BDWGC or tinygogc without that allocator/profile mechanism, so reproducing it is not an LLGo compatibility goal"
Expand Down
80 changes: 79 additions & 1 deletion test/goroot/proc_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ package goroot
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"testing"
"time"
Expand All @@ -30,8 +32,33 @@ func killProcessTree(cmd *exec.Cmd) {

func resourceMonitoringSupported() bool { return true }

var processRSSQueryTimeout = time.Second

func processGroupRSS(processGroupID int) (uint64, error) {
output, err := exec.Command("ps", "-axo", "pgid=,rss=").Output()
// A slow process-table query must not block the command's wait or
// timeout channels. A failed sample is retried on the next RSS tick.
ctx, cancel := context.WithTimeout(context.Background(), processRSSQueryTimeout)
defer cancel()
type queryResult struct {
output []byte
err error
}
result := make(chan queryResult, 1)
go func() {
output, err := exec.CommandContext(ctx, "ps", "-axo", "pgid=,rss=").Output()
result <- queryResult{output, err}
}()
var output []byte
var err error
select {
case query := <-result:
output, err = query.output, query.err
case <-ctx.Done():
return 0, ctx.Err()
}
if ctx.Err() != nil {
return 0, ctx.Err()
}
if err != nil {
return 0, err
}
Expand All @@ -58,6 +85,57 @@ func processGroupRSS(processGroupID int) (uint64, error) {
return totalKiB << 10, nil
}

func TestProcessGroupRSSQueryIsBounded(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "ps"), []byte("#!/bin/sh\nexec /bin/sleep 2\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
previous := processRSSQueryTimeout
processRSSQueryTimeout = 20 * time.Millisecond
t.Cleanup(func() { processRSSQueryTimeout = previous })
start := time.Now()
if _, err := processGroupRSS(os.Getpid()); !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("slow RSS query error = %v, want deadline exceeded", err)
}
if elapsed := time.Since(start); elapsed >= time.Second {
t.Fatalf("slow RSS query took %s, want bounded return", elapsed)
}
}

func TestRunProgramDeadlineAfterSlowRSSQuery(t *testing.T) {
disableSystemMemoryLimits(t)
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "ps"), []byte("#!/bin/sh\nexec /bin/sleep 2\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
previousTimeout, previousPoll := processRSSQueryTimeout, *flagRSSPoll
processRSSQueryTimeout, *flagRSSPoll = 400*time.Millisecond, 5*time.Millisecond
t.Cleanup(func() { processRSSQueryTimeout, *flagRSSPoll = previousTimeout, previousPoll })
// Both children exit while sampling is still running. Their actual wait
// completion time decides the deadline, rather than select's random pick.
for _, tc := range []struct {
name, sleep string
wantTimeout bool
}{
{"before-deadline", "0.05", false},
{"after-deadline", "0.25", true},
} {
t.Run(tc.name, func(t *testing.T) {
_, _, exitCode, _, err := runProgram(t.TempDir(), "/bin/sh", os.Environ(),
150*time.Millisecond, "-c", "sleep "+tc.sleep)
if tc.wantTimeout {
if err == nil || !strings.Contains(err.Error(), "timed out after") {
t.Fatalf("late child error = %v, want timeout", err)
}
} else if err != nil || exitCode != 0 {
t.Fatalf("completed child was reported as timed out: exit=%d, err=%v", exitCode, err)
}
})
}
}

func TestRunProgramWaitDelayCleansDescendant(t *testing.T) {
guardTestTimeout(t)
disableSystemMemoryLimits(t)
Expand Down
66 changes: 52 additions & 14 deletions test/goroot/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -822,10 +822,17 @@ func runCase(t *testing.T, repoRoot, goroot, goCmd, llgoBin string, tc testCase,
}

func boundedCaseEnv(goos, goarch string, tc testCase, extra []string) []string {
if goos == "linux" && goarch == "amd64" && tc.Directive == "runoutput" && tc.RelPath == "rangegen.go" {
if (goos == "linux" && goarch == "amd64" || goos == "windows" && goarch == "arm64") &&
tc.Directive == "runoutput" && tc.RelPath == "rangegen.go" {
// The generated compiler torture test exceeded the 4 GiB hosted
// runner guard. Bound Go and LLGo's heaps for this case only.
return upsertEnv(extra, "GOMEMLIMIT=3GiB")
limit := "3GiB"
if goos == "windows" {
// Leave room for ARM64 native LLVM/CodeView allocations, which
// are outside Go's soft memory limit.
limit = "1GiB"
}
return upsertEnv(extra, "GOMEMLIMIT="+limit)
}
return extra
}
Expand Down Expand Up @@ -1008,15 +1015,20 @@ func runProgram(dir, app string, env []string, timeout time.Duration, args ...st
}

waitCh := make(chan error, 1)
var waitFinished time.Time
go func() {
waitCh <- cmd.Wait()
err := cmd.Wait()
waitFinished = time.Now()
waitCh <- err
}()

var err error
var terminationErr error
var timeoutTimer *time.Timer
var timeoutCh <-chan time.Time
var deadline time.Time
if timeout > 0 {
deadline = time.Now().Add(timeout)
timeoutTimer = time.NewTimer(timeout)
timeoutCh = timeoutTimer.C
defer timeoutTimer.Stop()
Expand Down Expand Up @@ -1045,20 +1057,23 @@ func runProgram(dir, app string, env []string, timeout time.Duration, args ...st
}
var peakRSS uint64
for {
// Completion takes priority over a queued sampling tick or timeout,
// especially when a slow resource query outlives the direct child.
select {
case err = <-waitCh:
// ErrWaitDelay identifies a pipe-copy timeout after a successful
// direct child, but an ExitError masks it after a non-zero exit.
// Either error can therefore leave descendants alive.
if err != nil {
killProcessTree(cmd)
}
goto finished
goto waited
default:
}
select {
case <-timeoutCh:
goto timedOut
default:
}
select {
case err = <-waitCh:
goto waited
case <-timeoutCh:
terminationErr = fmt.Errorf("timed out after %s", timeout)
killProcessTree(cmd)
err = waitTerminatedProgram(cmd, waitCh, 10*time.Second)
goto finished
goto timedOut
case <-rssCh:
rssBytes, rssErr := processGroupRSS(cmd.Process.Pid)
if rssErr != nil {
Expand All @@ -1084,6 +1099,29 @@ func runProgram(dir, app string, env []string, timeout time.Duration, args ...st
}
}

timedOut:
select {
case err = <-waitCh:
goto waited
default:
}
terminationErr = fmt.Errorf("timed out after %s", timeout)
killProcessTree(cmd)
err = waitTerminatedProgram(cmd, waitCh, 10*time.Second)
goto finished
waited:
// Receiving waitCh also publishes waitFinished. Preserve the deadline
// for children that finish late, even if completion and timeout were both
// queued while a resource query was running.
if !deadline.IsZero() && waitFinished.After(deadline) {
terminationErr = fmt.Errorf("timed out after %s", timeout)
}
// ErrWaitDelay identifies a pipe-copy timeout after a successful direct
// child, but an ExitError masks it after a non-zero exit. Either error
// can therefore leave descendants alive.
if err != nil || terminationErr != nil {
killProcessTree(cmd)
}
finished:
if *flagRSSWarnMiB > 0 && peakRSS >= uint64(*flagRSSWarnMiB)<<20 {
fmt.Fprintf(os.Stderr, "goroot resource warning: %s peak process-group RSS %s\n", filepath.Base(app), formatBytes(peakRSS))
Expand Down
11 changes: 11 additions & 0 deletions test/goroot/runner_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ func TestObservedNotApplicableCasesAreGlobal(t *testing.T) {
repo := repoRoot(t)
cfg := loadNotApplicableConfig(t, repo, filepath.Join("test", "goroot", "notapplicable.yaml"))
cases := []testCase{
{RelPath: "fixedbugs/issue75764.go", Directive: "run"},
{RelPath: "deferfin.go", Directive: "run"},
{RelPath: "finprofiled.go", Directive: "run"},
{RelPath: "fixedbugs/issue24491b.go", Directive: "run"},
Expand Down Expand Up @@ -464,6 +465,14 @@ func TestWasmObservedResourceExceptions(t *testing.T) {
if timeout, _, match := cfg.MatchTimeout(version, "linux/amd64", rangegen); !match || timeout != 10*time.Minute {
t.Errorf("%s linux/amd64 rangegen timeout = %s, %v; want 10m, true", version, timeout, match)
}
if timeout, _, match := cfg.MatchTimeout(version, "windows-mingw/amd64", rangegen); !match || timeout != 10*time.Minute {
t.Errorf("%s Windows MinGW/amd64 rangegen timeout = %s, %v; want 10m, true", version, timeout, match)
}
for _, platform := range []string{"windows-msvc/arm64", "windows-mingw/arm64"} {
if timeout, _, match := cfg.MatchTimeout(version, platform, rangegen); !match || timeout != 10*time.Minute {
t.Errorf("%s %s rangegen timeout = %s, %v; want 10m, true", version, platform, timeout, match)
}
}
}
for _, tt := range []struct {
tc testCase
Expand Down Expand Up @@ -537,6 +546,8 @@ func TestBoundedCaseEnv(t *testing.T) {
}{
{"linux", "amd64", rangegen, nil, []string{"GOMEMLIMIT=3GiB"}},
{"linux", "amd64", rangegen, []string{"GOMEMLIMIT=1GiB"}, []string{"GOMEMLIMIT=3GiB"}},
{"windows", "arm64", rangegen, nil, []string{"GOMEMLIMIT=1GiB"}},
{"windows", "arm64", rangegen, []string{"GOMEMLIMIT=4GiB"}, []string{"GOMEMLIMIT=1GiB"}},
{"linux", "arm64", rangegen, nil, nil},
{"darwin", "arm64", rangegen, nil, nil},
{"linux", "amd64", testCase{RelPath: "other.go", Directive: "runoutput"}, nil, nil},
Expand Down
Loading
Loading