go: syscall: ForkExec with Ptrace flag causes runtime to be traced

Environment

> go version
go version go1.6.2 linux/amd64

> uname -a
Linux yoga 4.4.0-83-generic #106-Ubuntu SMP Mon Jun 26 17:54:43 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

> go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/bjames/Dropbox/go"
GORACE=""
GOROOT="/usr/lib/go-1.6"
GOTOOLDIR="/usr/lib/go-1.6/pkg/tool/linux_amd64"
GO15VENDOREXPERIMENT="1"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0"
CXX="g++"
CGO_ENABLED="1"

Issue

This is somewhat of a philosophical question about the design decisions made in the Go runtime, rather than a bug, per-se.

I’ve been trying to implement a simple strace-like program in Golang, using syscall.ForkExec with the Ptrace flag enabled - my expectation of this flag is that it enables tracing syscalls of the child process after the fork has occurred, something like the following (as I would write, if implementing the same program in C):

pid = fork();
switch( pid )
{
    // Error
    case -1:
    {
        printf( "Fork failed!" );
        break
    }

    // Child process.
    case 0:
    {
        // Enable tracing of this process.
        ptrace( PTRACE_TRACEME, 0, 0, 0 );

        // SIGSTOP to allow parent to catch all syscalls from execvp() onwards.
        kill( getpid(), SIGSTOP );
        return execvp( args[0], args );
    }

    // Parent process.
    default:
    {
        // Wait for child to SIGSTOP.
        waitpid( pid, &status, 0 );
        assert( WIFSTOPPED( status ) );

        // Set ptrace options.
        ptrace( PTRACE_SETOPTIONS, pid, 0, PTRACE_O_TRACESYSGOOD );

        // Child is currently entering a kill() syscall - restart and catch the exit of that syscall.
        ptrace( PTRACE_SYSCALL, pid, 0, 0 );
        waitpid( pid, &status, 0 );

        // Child is about to execvp() - start tracing...
        trace( pid );        
    }
}

void trace( int pid )
{
    // Loop until child exits.
    while( 1 )
    {
        // Restart child and wait for next syscall entry.
        ptrace( PTRACE_SYSCALL, pid, 0, 0 );
        waitpid( pid, &status, 0);

        // Check status for child exiting.
        ...

        // Print syscall entry details.
        ...

        // Restart child and wait for syscall exit.
        ptrace( PTRACE_SYSCALL, pid, 0, 0 );
        waitpid( pid, &status, 0 );

        // Check status for child exiting.
        ...

        // Print syscall exit (retval) details.
        ...
    }
}

However it doesn’t seem like the above is possible in Go due to the fact that the Ptrace flag is checked too early in https://golang.org/src/syscall/exec_linux.go (forkAndExecInChild()) and a number of (Go runtime) syscalls are performed between enabling tracing and the actual exec call.

Here’s a cut-down version of the code I’m using:

// Create process attributes.
attr := syscall.ProcAttr{
    ...
    Sys: &syscall.SysProcAttr{
        Ptrace:  true,
    },
}

// Fork/exec command.
pid, err := syscall.ForkExec( path, args, &attr )
...

// Wait on child.
var wstat syscall.WaitStatus
_, err = syscall.Wait4( pid, &wstat, 0, nil )
...

// Set Ptrace options
err = syscall.PtraceSetOptions( pid, syscall.PTRACE_O_TRACESYSGOOD )

// Trace child...
trace( pid )



func trace {

    for {
        // Restart child and wait for next syscall entry.
        err := syscall.PtraceSyscall( pid, 0 )
        ...
        var wstat syscall.WaitStatus
        _, err = syscall.Wait4( pid, &wstat, 0, nil )
        ...

        // Check status for child exiting.
        ...

        // Print syscall entry details.
        ...

        // Restart child and wait for next syscall entry.
        err := syscall.PtraceSyscall( pid, 0 )
        ...
        var wstat syscall.WaitStatus
        _, err = syscall.Wait4( pid, &wstat, 0, nil )
        ...

        // Check status for child exiting.
        ...

        // Print syscall exit (retval) details.
        ...
    }
}

Question

Why was this design decision made and what is the intended usage of the Ptrace flag? Is it the intended behaviour to trace the Go runtime in the child process before the exec? If so, that’s a bit counter-intuitive to the user.

Even better, is there a code example demonstrating the intended usage of the Ptrace flag and how one might implement strace using Go?

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Comments: 17 (12 by maintainers)

Most upvoted comments

Thanks @odeke-em - I added myself as a reviewer on there. Exciting, my first Go review is a LGTM on a @jessfraz change 😃

@lizrice I don’t know if you are interested, but if you can log into Gerrit with a gmail account, it’d be awesome to have you there as a reviewer and you can put your approvals or disapprovals on there, and then in merges, yours will be there on the record as a Go reviewer!

don’t know how qualified I am to say this, but lgtm!

Thanks for the shout-out @odeke-em 😃

Just reading the source quickly there, the Ptrace flag is checked as soon as the Clone syscall creates the child process. So yes, it’s tracing the syscalls that the Go runtime calls before the execve, but they are executed by the child process - so I think it’s right that they are included in the trace. If the ptrace wasn’t started until just before the execve, I think we’d just lose the ability to trace those syscalls, wouldn’t we?

@bjames2011 there was a presentation at Gophercon by Liz Rice @lizrice at https://medium.com/@lizrice/strace-in-60-lines-of-go-b4b76e3ecd64 “Strace in 60 lines of Go”, that might help you.