-
Notifications
You must be signed in to change notification settings - Fork 18.8k
Description
What version of Go are you using (go version)?
This is a problem under all most all Go versions, but this was 1.11
Does this issue reproduce with the latest release?
yes, but it's racy, see below.
What operating system and processor architecture are you using (go env)?
almost any linux post about 1.2 :-) and probably any Unix.
What did you do?
As part of writing strace in go for the u-root project I hit an interesting snag and have a question.
If you want to trace a child on Unix, the common path is to have the child of a fork call ptrace with PTRACE_TRACEME. The child will be stopped on the next system call and the parent is then able to take control via ptrace. This is why, as you may have noticed, the strace commands you run show an exec as the first system call.
What's this have to do with Go? The TRACEME rules are strict: only the parent can trace. So if, by some means, the goroutine running the tracing sometimes ends up being run by some process not the parent, you lose trace records with ENOENT, since a process not the parent is trying to trace the child.
I hit this in practice and the fix is easy: the process starting the Command with Ptrace set has to do a runtime.LockOSThread() (and of course unlock after) before starting the command, which ensures that the parent tracing the child is always the parent.
My question: should exec.Command, before calling fork or clone, call runtime.LockOSThread() when Ptrace is set?
On the one hand, it means we're adding a side effect of setting Ptrace: a LockOSThread(). OTOH, NOT setting leads to all kinds of unexpected problems: ptrace failures that seem to have no origin.
Here's a program from liz rice, modified to show the problem. If you use the go func the program rarely works correctly; comment out the go func and it rarely fails.
package main
import (
"fmt"
"os"
"os/exec"
"syscall"
)
func main() {
var regs syscall.PtraceRegs
cmd := exec.Command(os.Args[1], os.Args[2:]...)
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
cmd.SysProcAttr = &syscall.SysProcAttr{
Ptrace: true,
}
cmd.Start()
err := cmd.Wait()
if err != nil {
fmt.Printf("Wait returned: %v\n", err)
}
// comment out go func() to see correct behavior.
/** / go func() /**/{
pid := cmd.Process.Pid
var i int
var sysno uint64
for {
i++
err = syscall.PtraceGetRegs(pid, ®s)
if err != nil {
panic(err)
}
if (i & 1) == 1 {
sysno = regs.Orig_rax
fmt.Printf("%#016x %#016x", sysno, []uint64{
regs.Rdi, regs.Rsi, regs.Rdx,
regs.R10, regs.R8, regs.R9})
} else {
fmt.Printf("= %d\n", regs.Rax)
}
err = syscall.PtraceSyscall(pid, 0)
if err != nil {
panic(err)
}
_, err = syscall.Wait4(pid, nil, 0, nil)
if err != nil {
panic(err)
}
}
}/** /() /**/
}