diff --git a/fatrace.8 b/fatrace.8 index da89324..8559718 100644 --- a/fatrace.8 +++ b/fatrace.8 @@ -88,6 +88,10 @@ Add timestamp to events. When this option is given once, the format will be a human readable hour:minute:second.microsecond; when given twice, the timestamp is printed as seconds/microseconds since the epoch. +.TP +.B \-u\fR, \fB\-\-user +Add process user information to events, formatted as "[uid:gid]". + .TP .B \-p \fIPID\fR, \fB\-\-ignore\-pid=\fIPID Ignore events for this process ID. Can be specified multiple times. diff --git a/fatrace.c b/fatrace.c index d4f0dda..666b0c7 100644 --- a/fatrace.c +++ b/fatrace.c @@ -58,6 +58,7 @@ static long option_filter_mask = 0xffffffff; static long option_timeout = -1; static int option_current_mount = 0; static int option_timestamp = 0; +static int option_user = 0; static pid_t ignored_pids[1024]; static unsigned int ignored_pids_len = 0; static char* option_comm = NULL; @@ -226,14 +227,13 @@ static void print_event (const struct fanotify_event_metadata *data, const struct timeval *event_time) { - int proc_fd; int event_fd = data->fd; static char printbuf[100]; static char procname[100]; static int procname_pid = -1; static char pathname[PATH_MAX]; bool got_procname = false; - struct stat st; + struct stat proc_fd_stat = { .st_uid = -1 }; if ((data->mask & option_filter_mask) == 0 || !show_pid (data->pid)) { if (event_fd >= 0) @@ -241,11 +241,12 @@ print_event (const struct fanotify_event_metadata *data, return; } - /* read process name */ - snprintf (printbuf, sizeof (printbuf), "/proc/%i/comm", data->pid); - proc_fd = open (printbuf, O_RDONLY); + snprintf (printbuf, sizeof (printbuf), "/proc/%i", data->pid); + int proc_fd = open (printbuf, O_RDONLY | O_DIRECTORY); if (proc_fd >= 0) { - ssize_t len = read (proc_fd, procname, sizeof (procname)); + /* read process name */ + int procname_fd = openat (proc_fd, "comm", O_RDONLY); + ssize_t len = read (procname_fd, procname, sizeof (procname)); if (len >= 0) { while (len > 0 && procname[len-1] == '\n') len--; @@ -256,9 +257,18 @@ print_event (const struct fanotify_event_metadata *data, debug ("failed to read /proc/%i/comm", data->pid); } + close (procname_fd); + + /* get user and group */ + if (option_user) { + if (fstat (proc_fd, &proc_fd_stat) < 0) + debug ("failed to stat /proc/%i: %m", data->pid); + } + close (proc_fd); } else { - debug ("failed to open /proc/%i/comm: %m", data->pid); + debug ("failed to open /proc/%i: %m", data->pid); + procname[0] = '\0'; } /* /proc/pid/comm often goes away before processing the event; reuse previously cached value if pid still matches */ @@ -289,6 +299,7 @@ print_event (const struct fanotify_event_metadata *data, ssize_t len = readlink (printbuf, pathname, sizeof (pathname)); if (len < 0) { /* fall back to the device/inode */ + struct stat st; if (fstat (event_fd, &st) < 0) err (EXIT_FAILURE, "stat"); snprintf (pathname, sizeof (pathname), "device %i:%i inode %ld\n", major (st.st_dev), minor (st.st_dev), st.st_ino); @@ -308,7 +319,14 @@ print_event (const struct fanotify_event_metadata *data, } else if (option_timestamp == 2) { printf ("%li.%06li ", event_time->tv_sec, event_time->tv_usec); } - printf ("%s(%i): %-3s %s\n", procname[0] == '\0' ? "unknown" : procname, data->pid, mask2str (data->mask), pathname); + + /* print user and group */ + if (option_user && proc_fd_stat.st_uid != (uid_t)-1) + snprintf(printbuf, sizeof printbuf, " [%u:%u]", proc_fd_stat.st_uid, proc_fd_stat.st_gid); + else + printbuf[0] = '\0'; + + printf ("%s(%i)%s: %-3s %s\n", procname[0] == '\0' ? "unknown" : procname, data->pid, printbuf, mask2str (data->mask), pathname); } static void @@ -412,6 +430,7 @@ help (void) " -o FILE, --output=FILE\tWrite events to a file instead of standard output.\n" " -s SECONDS, --seconds=SECONDS\tStop after the given number of seconds.\n" " -t, --timestamp\t\tAdd timestamp to events. Give twice for seconds since the epoch.\n" +" -u, --user\t\t\tAdd user ID and group ID to events.\n" " -p PID, --ignore-pid PID\tIgnore events for this process ID. Can be specified multiple times.\n" " -f TYPES, --filter=TYPES\tShow only the given event types; choose from C, R, O, or W, e. g. --filter=OC.\n" " -C COMM, --command=COMM\tShow only events for this command.\n" @@ -436,6 +455,7 @@ parse_args (int argc, char** argv) {"output", required_argument, 0, 'o'}, {"seconds", required_argument, 0, 's'}, {"timestamp", no_argument, 0, 't'}, + {"user", no_argument, 0, 'u'}, {"ignore-pid", required_argument, 0, 'p'}, {"filter", required_argument, 0, 'f'}, {"command", required_argument, 0, 'C'}, @@ -444,7 +464,7 @@ parse_args (int argc, char** argv) }; while (1) { - c = getopt_long (argc, argv, "C:co:s:tp:f:h", long_options, NULL); + c = getopt_long (argc, argv, "C:co:s:tup:f:h", long_options, NULL); if (c == -1) break; @@ -462,6 +482,10 @@ parse_args (int argc, char** argv) option_output = strdup (optarg); break; + case 'u': + option_user = 1; + break; + case 'f': j = 0; option_filter_mask = 0; diff --git a/tests/fatrace-user b/tests/fatrace-user new file mode 100755 index 0000000..df704f2 --- /dev/null +++ b/tests/fatrace-user @@ -0,0 +1,64 @@ +#!/bin/sh +set -euC + +USER=nobody +ROOT_LOG="\\[0:0\\]" +USER_LOG="\\[$(id -u $USER):$(id -g $USER)\\]" + +mkdir -m 777 tmp +trap "rm -rf tmp" EXIT INT QUIT PIPE + +LOG="$AUTOPKGTEST_TMP/fatrace.log" +echo "starting fatrace..." +fatrace --current-mount --user -s 2 -o $LOG & +sleep 1 + +echo "read a file as root ..." +head NEWS > /dev/null + +echo "read a file as user ..." +runuser -u $USER tail NEWS > /dev/null + +echo "create/remove a file as root..." +TEST_FILE_ROOT=testroot.txt +touch "$TEST_FILE_ROOT" +rm "$TEST_FILE_ROOT" + +echo "create/remove a file as usr..." +TEST_FILE_USER=tmp/test$USER.txt +runuser -u $USER touch "$TEST_FILE_USER" +runuser -u $USER rm "$TEST_FILE_USER" + +echo "waiting for fatrace..." +wait + +echo "checking log..." +RC=0 +check_log() { + if ! grep -q "$1" $LOG; then + echo "$1 not found in log" >&2 + ((RC=RC+1)) + fi +} + +# accessing the NEWS file as root +check_log "^head([0-9]*) $ROOT_LOG: RC\?O\? \+$(pwd)/NEWS$" + +# accessing the NEWS file as user +check_log "^tail([0-9]*) $USER_LOG: RC\?O\? \+$(pwd)/NEWS$" + +# file creation as root +TEST_FILE_ROOT=$(realpath "$TEST_FILE_ROOT") +check_log "^touch([0-9]*) $ROOT_LOG: C\?W\?O \+$TEST_FILE_ROOT" + +TEST_FILE_USER=$(realpath "$TEST_FILE_USER") +check_log "^touch([0-9]*) $USER_LOG: C\?W\?O \+$TEST_FILE_USER" + +if [ $RC -ne 0 ]; then + echo "$RC checks failed -- log:" >&2 + echo "===================" >&2 + cat $LOG >&2 + echo "===================" >&2 +fi + +exit $RC diff --git a/tests/run b/tests/run index 7730ec8..f4af9d4 100755 --- a/tests/run +++ b/tests/run @@ -4,7 +4,7 @@ set -eu MYDIR=$(dirname $(readlink -f "$0")) export PATH=$(pwd):$PATH -for t in fatrace fatrace-currentmount fatrace-btrfs; do +for t in fatrace fatrace-currentmount fatrace-btrfs fatrace-user; do export AUTOPKGTEST_TMP=$(mktemp -d) echo "===== $t ====" "$MYDIR"/$t