14

I'm writing a tool that calls through to DTrace to trace the program that the user specifies.

If my tool uses dtrace -c to run the program as a subprocess of DTrace, not only can I not pass any arguments to the program, but the program runs with all the privileges of DTrace—that is, as root (I'm on Mac OS X). This makes certain things that should work break, and obviously makes a great many things that shouldn't work possible.

The other solution I know of is to start the program myself, pause it by sending it SIGSTOP, pass its PID to dtrace -p, then continue it by sending it SIGCONT. The problem is that either the program runs for a few seconds without being traced while DTrace gathers the symbol information or, if I sleep for a few seconds before continuing the process, DTrace complains that objc<pid>:<class>:<method>:entry matches no probes.

Is there a way that I can run the program under the user's account, not as root, but still have DTrace able to trace it from the beginning?

Peter Hosey
  • 95,783
  • 15
  • 211
  • 370

8 Answers8

6

Something like sudo dtruss -f sudo -u <original username> <command> has worked for me, but I felt bad about it afterwards.

I filed a Radar bug about it and had it closed as a duplicate of #5108629.

alex strange
  • 1,249
  • 9
  • 9
5

This script takes the name of the executable (for an app this is the info.plist's CFBundleExecutable) you want to monitor to DTrace as a parameter (you can then launch the target app after this script is running):

string gTarget;     /* the name of the target executable */

dtrace:::BEGIN
{
    gTarget = $$1;  /* get the target execname from 1st DTrace parameter */

    /*
    * Note: DTrace's execname is limited to 15 characters so if $$1 has more
    * than 15 characters the simple string comparison "($$1 == execname)"
    * will fail. We work around this by copying the parameter passed in $$1
    * to gTarget and truncating that to 15 characters.
    */

    gTarget[15] = 0;        /* truncate to 15 bytes */
    gTargetPID = -1;        /* invalidate target pid */
}

/*
* capture target launch (success)
*/
proc:::exec-success
/
    gTarget == execname
/
{
    gTargetPID = pid;
}

/*
*   detect when our target exits
*/
syscall::*exit:entry
/
    pid == gTargetPID
/
{
    gTargetPID = -1;        /* invalidate target pid */
}

/*
* capture open arguments
*/
syscall::open*:entry
/
    ((pid == gTargetPID) || progenyof(gTargetPID))
/
{
    self->arg0 = arg0;
    self->arg1 = arg1;
}

/*
* track opens
*/
syscall::open*:return
/
    ((pid == gTargetPID) || progenyof(gTargetPID))
/
{
    this->op_kind = ((self->arg1 & O_ACCMODE) == O_RDONLY) ? "READ" : "WRITE";
    this->path0 = self->arg0 ? copyinstr(self->arg0) : "<nil>";

    printf("open for %s: <%s> #%d",
        this->op_kind,
        this->path0,
        arg0);
}
geowar
  • 4,397
  • 1
  • 28
  • 24
  • This is great, except that when doing this dtrace does not find any of the pid provider trace point. For example, I get an error: "probe description pid*::confstr:return does not match any probes" – Droopycom Mar 10 '16 at 08:35
4

Well, this is a bit old, but why not :-)..

I don't think there is a way to do this simply from command line, but as suggested, a simple launcher application, such as the following, would do it. The manual attaching could of course also be replaced with a few calls to libdtrace.

int main(int argc, char *argv[]) {
    pid_t pid = fork();
    if(pid == 0) {
        setuid(123);
        seteuid(123);
        ptrace(PT_TRACE_ME, 0, NULL, 0);
        execl("/bin/ls", "/bin/ls", NULL);
    } else if(pid > 0) {
        int status;
        wait(&status);

        printf("Process %d started. Attach now, and click enter.\n", pid);
        getchar();

        ptrace(PT_CONTINUE, pid, (caddr_t) 1, 0);
    }

    return 0;
}
Sami
  • 3,263
  • 3
  • 29
  • 37
3

If the other answer doesn't work for you, can you run the program in gdb, break in main (or even earlier), get the pid, and start the script? I've tried that in the past and it seemed to work.

kperryua
  • 10,524
  • 1
  • 38
  • 24
2

Create a launcher program that will wait for a signal of some sort (not necessarily a literal signal, just an indication that it's ready), then exec() your target. Now dtrace -p the launcher program, and once dtrace is up, let the launcher go.

bdonlan
  • 224,562
  • 31
  • 268
  • 324
  • 1
    Even better: roll it all into one. Create a pipe, fork, have the child wait on the pipe, dtrace -p CHILD_PID, write to the pipe, child wakes up and calls exec. – bstpierre Jul 30 '09 at 03:41
  • Sounds promising. I don't know that it will work, though: My tool is written in Python, and it's designed to trace Cocoa programs. Without any Cocoa in my tool, I think I'll still get the error that the objc provider matches no probes. But I'll try it tomorrow. – Peter Hosey Jul 30 '09 at 06:19
1

dtruss has the -n option where you can specify name of process you want to trace, without starting it (Credit to latter part of @kenorb's answer at https://stackoverflow.com/a/11706251/970301). So something like the following should do it:

sudo dtruss -n "$program"
$program
Community
  • 1
  • 1
Vivek
  • 564
  • 4
  • 13
1

There exists a tool darwin-debug that ships in Apple's CLT LLDB.framework which will spawn your program and pause it before it does anything. You then read the pid out of the unix socket you pass as an argument, and after attaching the debugger/dtrace you continue the process.

darwin-debug will exec itself into a child process <PROGRAM> that is
halted for debugging. It does this by using posix_spawn() along with
darwin specific posix_spawn flags that allows exec only (no fork), and
stop at the program entry point. Any program arguments <PROGRAM-ARG> are
passed on to the exec as the arguments for the new process. The current
environment will be passed to the new process unless the "--no-env"
option is used. A unix socket must be supplied using the
--unix-socket=<SOCKET> option so the calling program can handshake with
this process and get its process id.
Camden Narzt
  • 2,271
  • 1
  • 23
  • 42
0

See my answer on related question "How can get dtrace to run the traced command with non-root priviledges?" [sic].

Essentially, you can start a (non-root) background process which waits 1sec for DTrace to start up (sorry for race condition), and snoops the PID of that process.

sudo true && \
(sleep 1; cat /etc/hosts) &; \
sudo dtrace -n 'syscall:::entry /pid == $1/ {@[probefunc] = count();}' $! \
&& kill $!

Full explanation in linked answer.

Birchlabs
  • 7,437
  • 5
  • 35
  • 54