Files
gasm-sdk/debug/target_linux_riscv64.go
T
petrbalvin b5d6f1b46a fix(debug): target the traced thread and keep the kill from ever blocking
The Go runtime can migrate the debuggee's target-mode goroutine off the
process leader before PTRACE_TRACEME, which left the trace relation on a
thread the session never addressed: its stops starved the waits on the
leader, and a kill sequence that resumed nothing and then blocked in
Wait4 hung the whole package.  The debuggee now reports the traced thread
in the launch handshake and parks with a thread-directed stop, every
ptrace request and wait addresses that thread, a SIGURG arriving on a
single-step resumes it as a single-step again instead of letting the
tracee run uncontrolled, a resume rejected with ESRCH lifts a group-stop
with SIGCONT and retries once, and Kill resumes, kills and reaps through
non-blocking waits so it returns for a tracee in any state.

Assisted-by: GLM 5.3
2026-10-07 13:46:18 +02:00

123 lines
3.2 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && riscv64
package debug
import (
"fmt"
"os"
"runtime"
"syscall"
"unsafe"
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
)
// RunTarget is the debuggee entry point (gasm debug --target). A failure
// is marked in the handshake directory before the process exits, so the
// debugger's readiness poll fails fast on a dead debuggee instead of
// waiting out its whole budget.
func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
err := runTarget(asmPath, funcName, argsFile, tmpDir)
if err != nil {
markDead(tmpDir, err.Error())
}
return err
}
func runTarget(asmPath, funcName, argsFile, tmpDir string) error {
src, err := os.ReadFile(asmPath)
if err != nil {
return fmt.Errorf("debug target: %w", err)
}
file, errs := parser.Parse(asmPath, string(src))
if len(errs) > 0 {
return fmt.Errorf("debug target: parse: %v", errs[0])
}
img, err := asm.AssembleFileRISCV(file)
if err != nil {
return fmt.Errorf("debug target: assemble: %w", err)
}
var fl *asm.FuncLayout
for i := range img.Funcs {
if img.Funcs[i].Name == funcName {
fl = &img.Funcs[i]
break
}
}
if fl == nil {
return fmt.Errorf("debug target: function %q not found", funcName)
}
code := img.Bytes()
exec, err := mapRWX(code)
if err != nil {
return fmt.Errorf("debug target: mmap: %w", err)
}
codeBase := uintptr(unsafe.Pointer(&exec[0]))
if err := os.WriteFile(tmpDir+"/codebase", []byte(fmt.Sprintf("%d", codeBase)), 0o644); err != nil {
return fmt.Errorf("debug target: write codebase: %w", err)
}
meta := fmt.Sprintf("%d %d %d", fl.Offset, fl.Size, fl.Args)
os.WriteFile(tmpDir+"/funcmeta", []byte(meta), 0o644)
labelsFile, _ := os.Create(tmpDir + "/labels")
if labelsFile != nil {
for label, off := range fl.Labels {
fmt.Fprintf(labelsFile, "%s %d\n", label, off)
}
labelsFile.Close()
}
args, err := os.ReadFile(argsFile)
if err != nil {
return fmt.Errorf("debug target: read args: %w", err)
}
if len(args) < fl.Args {
padded := make([]byte, fl.Args)
copy(padded, args)
args = padded
}
bufSpecFile := tmpDir + "/bufspec"
if bufSpec, err := os.ReadFile(bufSpecFile); err == nil && len(bufSpec) > 0 {
args, err = setupBuffers(string(bufSpec), args, tmpDir)
if err != nil {
return fmt.Errorf("debug target: setup buffers: %w", err)
}
}
runtime.LockOSThread()
if _, _, errno := syscall.Syscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0); errno != 0 {
return fmt.Errorf("debug target: PTRACE_TRACEME: %v", errno)
}
// The tracer must address the thread that called PTRACE_TRACEME, which
// is not necessarily the process leader: report it before readiness.
os.WriteFile(tmpDir+"/tracetid", []byte(fmt.Sprintf("%d", syscall.Gettid())), 0o644)
os.WriteFile(tmpDir+"/ready", []byte("ok"), 0o644)
parkSelf()
os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644)
parkSelf()
fnAddr := codeBase + uintptr(fl.Offset)
stackArgs := make([]byte, fl.Args)
copy(stackArgs, args)
_, callErr := verify.Call(fnAddr, stackArgs)
if callErr != nil {
os.Exit(1)
}
os.Exit(0)
return nil
}