From cb398d949866dea4b8953a584012bc559e724679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Sat, 29 Aug 2026 14:16:04 +0200 Subject: [PATCH] feat(debug): headless script mode with timeout watchdog --- cmd/gasm/debug_linux.go | 37 +++++++++++++++++++++++++++++++++++-- debug/repl.go | 9 +++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/cmd/gasm/debug_linux.go b/cmd/gasm/debug_linux.go index 937b1de..a1b68b5 100644 --- a/cmd/gasm/debug_linux.go +++ b/cmd/gasm/debug_linux.go @@ -7,9 +7,11 @@ package main import ( "fmt" + "io" "os" "sort" "strings" + "time" "sourcedock.dev/petrbalvin/gasm-devkit/debug" "sourcedock.dev/petrbalvin/gasm-devkit/verify" @@ -34,6 +36,8 @@ REPL commands: funcName := fs.String("func", "", "function to debug") argsFile := fs.String("args", "", "file containing the ABI0 argument block") bufSpec := fs.String("buf", "", "buffer specification: name:size:pattern[,name:size:pattern...] where pattern is zero, ones, seq, or hex") + script := fs.String("script", "", "run REPL commands from a file (one per line) and exit; '-' reads stdin") + timeout := fs.Duration("timeout", 0, "kill the debuggee after this duration (e.g. 30s); for headless --script runs") fs.Parse(args) // --- Debuggee mode (internal, spawned by the debugger) --- @@ -57,6 +61,18 @@ REPL commands: } path := fs.Arg(0) + // The watchdog is armed before anything can block: ptrace attach and a + // continued kernel loop both hang the run when the environment forbids + // tracing or the kernel loops forever, and neither is interruptible from + // the inside. + if *timeout > 0 { + go func() { + time.Sleep(*timeout) + fmt.Fprintf(os.Stderr, "gasm debug: timeout (%s) — killing the debuggee\n", *timeout) + os.Exit(3) + }() + } + // Load the kernel to extract function metadata and labels. k, err := verify.Load(path) if err != nil { @@ -157,12 +173,29 @@ REPL commands: bm := debug.NewBreakpoints(sess) fmt.Printf("gasm debug: %s in %s (pid %d)\n", *funcName, path, sess.Pid()) - // Convert the line table for the REPL. + // Convert the line table for the command loop. var srcLines []debug.SourceLine for _, le := range fl.Lines { srcLines = append(srcLines, debug.SourceLine{Offset: le.Offset, Line: le.Line}) } - debug.REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, labels, srcLines) + + // Headless mode: run the script through the normal command loop and + // exit. The watchdog armed above covers launch, continue and step. + var in io.Reader = os.Stdin + if *script != "" { + if *script == "-" { + in = os.Stdin + } else { + f, err := os.Open(*script) + if err != nil { + fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err) + return 1 + } + defer f.Close() + in = f + } + } + debug.REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, labels, srcLines, in) return 0 } diff --git a/debug/repl.go b/debug/repl.go index 28ed1b3..c1d870b 100644 --- a/debug/repl.go +++ b/debug/repl.go @@ -8,7 +8,7 @@ package debug import ( "bufio" "fmt" - "os" + "io" "sort" "strconv" "strings" @@ -26,14 +26,15 @@ type SourceLine struct { Line int } -// REPL runs the interactive debugger loop. -func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine) { +// REPL runs the interactive debugger loop, reading commands from in (pass +// os.Stdin interactively, or a bytes.Reader/script file for headless runs). +func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine, in io.Reader) { entryAddr := codeBase + uint64(funcOffset) fmt.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize) fmt.Println("commands: break | step [n] | continue | disas [n] | regs | where | x [len] | w | labels | quit") - scanner := bufio.NewScanner(os.Stdin) + scanner := bufio.NewScanner(in) for { fmt.Print("(gasm) ")