3 # Copyright (c) 2014-2018 Devin Teske
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
9 # 1. Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 # 2. Redistributions in binary form must reproduce the above copyright
12 # notice, this list of conditions and the following disclaimer in the
13 # documentation and/or other materials provided with the distribution.
15 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27 ############################################################ IDENT(1)
29 # $Title: Watch processes as they trigger a particular DTrace probe $
32 ############################################################ CONFIGURATION
35 # DTrace pragma settings
40 option switchrate=10hz
46 : ${DWATCH_PROFILES_PATH="/usr/libexec/dwatch:/usr/local/libexec/dwatch"}
48 ############################################################ GLOBALS
50 VERSION='$Version: 1.2 $' # -V
52 pgm="${0##*/}" # Program basename
55 # Command-line arguments
60 # Command-line defaults
63 _MAX_DEPTH=64 # -K num
66 # Command-line options
70 [ -t 1 ] && CONSOLE=1 # -y
72 CUSTOM_DETAILS= # -E code
73 CUSTOM_TEST= # -t test
75 DESTRUCTIVE_ACTIONS= # -w
79 EXIT_AFTER_COMPILE= # -e
86 MAX_ARGS=$_MAX_ARGS # -B num
87 MAX_DEPTH=$_MAX_DEPTH # -K num
92 PROBE_TYPE= # -f -m -n -P
99 USE_PROFILE= # -X profile
113 EVENT_TAG='printf("%d.%d %s[%d]: ",
114 this->uid0, this->gid0, execname, this->pid0);'
124 export SUDO_PROMPT="[sudo] Password:"
127 ############################################################ FUNCTIONS
129 ansi() { local fmt="$2 $4"; [ "$CONSOLE" ] && fmt="\\033[$1m$2\\033[$3m $4";
130 shift 4; printf "$fmt\n" "$@"; }
131 die() { exec >&2; [ "$*" ] && echo "$pgm:" "$@"; exit $FAILURE; }
132 info() { [ "$QUIET" ] || ansi 35 "INFO" 39 "$@" >&2; }
136 local optfmt="\t%-10s %s\n"
138 [ "$*" ] && printf "%s: %s\n" "$pgm" "$*"
139 printf "Usage: %s [-1defFmnPqRvVwxy] [%s] [%s] [%s] [%s]\n" "$pgm" \
140 "-B num" "-E code" "-g group" "-j jail"
141 printf "\t [%s] [%s] [%s] [%s] [%s] [%s]\n" \
142 "-k name" "-K num" "-N count" "-o file" "-O cmd" "-p pid"
143 printf "\t [%s] [%s] [%s] [%s] [%s] [%s]\n" \
144 "-r regex" "-t test" "-T time" "-u user" "-X profile" \
146 printf "\t probe[,...] [args ...]\n"
147 printf " %s -l [-fmnPqy] [-r regex] [probe ...]\n" "$pgm"
148 printf " %s -Q [-1qy] [-r regex]\n" "$pgm"
150 printf "$optfmt" "-1" \
151 "Print one line per process/profile (Default; disables \`-R')."
152 printf "$optfmt" "-B num" \
153 "Maximum process arguments to display (Default $_MAX_ARGS)."
154 printf "$optfmt" "-d" \
155 "Debug. Send dtrace(1) script to stdout instead of executing."
156 printf "$optfmt" "-e" \
157 "Exit after compiling request but prior to enabling probes."
158 printf "$optfmt" "-E code" \
159 "DTrace code for event details. If \`-', read from stdin."
160 printf "$optfmt" "-f" \
161 "Enable probe matching the specified function name."
162 printf "$optfmt" "-F" \
163 "Coalesce trace output by function."
164 printf "$optfmt" "-g group" \
165 "Group filter. Only show processes matching group name/gid."
166 printf "$optfmt" "-j jail" \
167 "Jail filter. Only show processes matching jail name/jid."
168 printf "$optfmt" "-k name" \
169 "Only show processes matching name."
170 printf "$optfmt" "-K num" \
171 "Maximum directory depth to display (Default $_MAX_DEPTH)."
172 printf "$optfmt" "-l" \
173 "List available probes on standard output and exit."
174 printf "$optfmt" "-m" \
175 "Enable probe matching the specified module name."
176 printf "$optfmt" "-n" \
177 "Enable probe matching the specified probe name."
178 printf "$optfmt" "-N count" \
179 "Exit after count matching entries (Default 0 for disabled)."
180 printf "$optfmt" "-o file" \
181 "Set output file. If \`-', the path \`/dev/stdout' is used."
182 printf "$optfmt" "-O cmd" \
183 "Execute cmd for each event."
184 printf "$optfmt" "-p pid" \
185 "Process id filter. Only show processes with matching pid."
186 printf "$optfmt" "-P" \
187 "Enable probe matching the specified provider name."
188 printf "$optfmt" "-q" \
189 "Quiet. Hide informational messages and all dtrace(1) errors."
190 printf "$optfmt" "-Q" \
191 "List available profiles in DWATCH_PROFILES_PATH and exit."
192 printf "$optfmt" "-r regex" \
193 "Filter. Only show blocks matching awk(1) regular expression."
194 printf "$optfmt" "-R" \
195 "Show parent, grandparent, and ancestor of process."
196 printf "$optfmt" "-t test" \
197 "Test clause (predicate) to limit events (Default none)."
198 printf "$optfmt" "-T time" \
199 "Timeout. Format is \`\#[smhd]' or simply \`\#' for seconds."
200 printf "$optfmt" "-u user" \
201 "User filter. Only show processes matching user name/uid."
202 printf "$optfmt" "-v" \
203 "Verbose. Show all errors from dtrace(1)."
204 printf "$optfmt" "-V" \
205 "Report dwatch version on standard output and exit."
206 printf "$optfmt" "-w" \
207 "Permit destructive actions (copyout*, stop, panic, etc.)."
208 printf "$optfmt" "-x" \
209 "Trace. Print \`<probe-id>' when a probe is triggered."
210 printf "$optfmt" "-X profile" \
211 "Load profile name from DWATCH_PROFILES_PATH."
212 printf "$optfmt" "-y" \
213 "Always treat stdout as console (enable colors/columns/etc.)."
214 printf "$optfmt" "-z regex" \
215 "Only show processes matching awk(1) regular expression."
224 if [ "$1" = "-t" ]; then
226 [ "$TIMEOUT" ] && timeout=1
233 # Filter dtrace(1) stderr while preserving exit status
238 ( trap 'echo $? >&$to_status' EXIT
239 eval $SUDO ${timeout:+timeout \"\$TIMEOUT\"} dtrace \
240 \"\$@\" 2>&1 ${QUIET:+2> /dev/null} >&$stdout
241 ) | dtrace_stderr_filter >&2
247 dtrace_stderr_filter()
249 if [ "$VERBOSE" ]; then
255 awk ' # Start awk(1) stderr-filter
256 /[[:digit:]]+ drops? on CPU [[:digit:]]+/ { next }
257 /failed to write to <stdout>: No such file or directory/ { next }
258 /failed to write to <stdout>: Broken pipe/ { next }
259 /processing aborted: Broken pipe/ { next }
260 /invalid address \(0x[[:xdigit:]]+\) in action #[[:digit:]]+/ { next }
261 /out of scratch space in action #[[:digit:]]+/ { next }
262 /^Bus error$/ { next }
269 local OPTIND=1 OPTARG flag
272 while getopts t: flag; do
277 shift $(( $OPTIND - 1 ))
287 dtrace_cmd -l | awk -v probe="$probe" -v type="$type" '
288 # Start awk(1) processor
289 #################################################### BEGIN
290 BEGIN { getline dtrace_header }
291 #################################################### FUNCTIONS
292 function dump(unused1,unused2) {
294 if (NcF[n] == 1) f = N2F[n]
295 if (NcM[n] == 1) m = N2M[n]
296 if (NcP[n] == 1) p = N2P[n]
298 if (FcM[f] == 1) m = F2M[f]
299 if (FcP[f] == 1) p = F2P[f]
300 if (FcN[f] == 0 && found) n = "entry"
302 if (McP[m] == 1) p = M2P[m]
304 printf "%s:%s:%s:%s\n", p, m, f, n
307 function inFMP() { return probe in F || probe in M || probe in P }
308 function inNMP() { return probe in N || probe in M || probe in P }
309 function inNFP() { return probe in N || probe in F || probe in P }
310 function inNFM() { return probe in N || probe in F || probe in M }
311 function diva(value, peerA, peerB, peerC) {
312 return value >= peerA && value >= peerB && value >= peerC
314 #################################################### MAIN
315 type == "name" && $NF != probe { next }
316 type == "function" && NF >=4 && $(NF-1) != probe { next }
317 type == "module" && NF == 5 && $(NF-2) != probe { next }
318 type == "provider" && $2 != probe { next }
319 type || $2 == probe || $3 == probe || $4 == probe || $5 == probe {
321 M[_m = (NF >= 5 ? $(NF-2) : "")]++
322 F[_f = (NF >= 4 ? $(NF-1) : "")]++
324 if (N2F[_n] != _f) NcF[_n]++; N2F[_n] = _f
325 if (N2M[_n] != _m) NcM[_n]++; N2M[_n] = _m
326 if (N2P[_n] != _p) NcP[_n]++; N2P[_n] = _p
327 if (_n !~ /entry|return/) {
328 if (F2N[_f] != _n) FcN[_f]++
331 if (F2M[_f] != _m) FcM[_f]++; F2M[_f] = _m
332 if (F2P[_f] != _p) FcP[_f]++; F2P[_f] = _p
333 if (M2P[_m] != _p) McP[_m]++; M2P[_m] = _p
335 #################################################### END
337 if (type == "name") dump(n = probe, found = probe in N)
338 if (type == "function") dump(f = probe, found = probe in F)
339 if (type == "module") dump(m = probe, found = probe in M)
340 if (type == "provider") dump(p = probe, found = probe in P)
343 if (!inFMP()) dump(n = probe)
344 if (diva(F[probe], N[probe], M[probe], P[probe]))
346 if (diva(M[probe], N[probe], F[probe], P[probe]))
348 if (diva(P[probe], N[probe], F[probe], M[probe]))
350 dump(n = probe) # N is the diva
351 } else if (probe in F) {
353 if (!inNMP()) dump(f = probe)
354 if (diva(N[probe], F[probe], M[probe], P[probe]))
356 if (diva(M[probe], F[probe], N[probe], P[probe]))
358 if (diva(P[probe], F[probe], N[probe], M[probe]))
360 dump(f = probe) # F is the diva
361 } else if (probe in M) {
363 if (!inNFP()) dump(m = probe)
364 if (diva(N[probe], M[probe], F[probe], P[probe]))
366 if (diva(F[probe], M[probe], N[probe], P[probe]))
368 if (diva(P[probe], M[probe], N[probe], F[probe]))
370 dump(m = probe) # M is the diva
371 } else if (probe in P) {
373 if (!inNFM()) dump(p = probe)
374 if (diva(N[probe], P[probe], F[probe], M[probe]))
376 if (diva(F[probe], P[probe], N[probe], M[probe]))
378 if (diva(M[probe], P[probe], N[probe], F[probe]))
380 dump(p = probe) # P is the diva
382 if (!found) print probe
390 local OPTIND=1 OPTARG flag
391 local column=0 header="PROVIDER:MODULE:FUNCTION:NAME"
392 local filter= quiet= type=
394 while getopts f:qt: flag; do
396 f) filter="$OPTARG" ;;
401 shift $(( $OPTIND - 1 ))
403 if [ $# -eq 0 ]; then
405 provider) column=1 header="PROVIDER" ;;
406 module) column=2 header="MODULE" ;;
407 function) column=3 header="FUNCTION" ;;
408 name) column=4 header="NAME" ;;
412 [ "$quiet" ] || echo "$header"
416 arg=$( expand_probe -t "$type" -- "$arg" )
417 probe="$probe${probe:+, }$arg"
420 dtrace_cmd -l${probe:+n "$probe"} | awk -v pattern="$(
421 # Prevent backslashes from being lost
422 echo "$filter" | awk 'gsub(/\\/,"&&")||1'
423 )" -v want="$column" -v console="$CONSOLE" '
424 BEGIN { getline dtrace_header }
425 function ans(seq) { return console ? "\033[" seq "m" : "" }
426 NF > 3 && $(NF-1) ~ /^#/ { next }
427 !_[$0 = column[0] = sprintf("%s:%s:%s:%s",
429 column[2] = (NF >= 5 ? $(NF-2) : ""),
430 column[3] = (NF >= 4 ? $(NF-1) : ""),
431 column[4] = $NF)]++ &&
432 !__[$0 = column[want]]++ &&
433 gsub(pattern, ans("31;1") "&" ans("39;22")) {
436 END { close("sort") }
444 local OPTIND=1 OPTARG flag
445 local filter= oneline= quiet=
447 while getopts 1f:q flag; do
450 f) filter="$OPTARG" ;;
454 shift $(( $OPTIND - 1 ))
456 # Prevent backslashes from being lost
457 filter=$( echo "$filter" | awk 'gsub(/\\/,"&&")||1' )
459 # Build a list of profiles available
462 for dir in $DWATCH_PROFILES_PATH; do
463 [ -d "$dir" ] || continue
464 for path in $dir/*; do
465 [ -f "$path" ] || continue
467 [ "$name" = "${name%%[!0-9A-Za-z_-]*}" ] ||
474 # Get the longest profile name
475 local longest_profile_name
476 longest_profile_name=$( echo "$profiles" |
477 awk -v N=0 '(L = length($0)) > N { N = L } END { print N }' )
479 # Get the width of the terminal
480 local max_size="$( stty size 2> /dev/null )"
482 local max_width="${max_size#*[$IFS]}"
484 # Determine how many columns we can display
485 local x=$longest_profile_name ncols=1
486 [ "$QUIET" ] || x=$(( $x + 8 )) # Accommodate leading tab character
487 x=$(( $x + 3 + $longest_profile_name )) # Preload end of next column
488 while [ $x -lt $max_width ]; do
489 ncols=$(( $ncols + 1 ))
490 x=$(( $x + 3 + $longest_profile_name ))
493 # Output single lines if sent to a pipe
494 if [ "$oneline" ]; then
495 echo "$profiles" | awk -v filter="$filter" -v cons="$CONSOLE" '
496 function ans(s) { return cons ? "\033[" s "m" : "" }
497 gsub(filter, ans("31;1") "&" ans("39;22"))
502 [ "$quiet" ] || echo PROFILES:
503 echo "$profiles" | awk \
504 -v colsize=$longest_profile_name \
505 -v console="$CONSOLE" \
508 -v filter="$filter" \
509 ' # Begin awk(1) processor
510 function ans(seq) { return console ? "\033[" seq "m" : "" }
513 replace = ans("31;1") "&" ans("39;22")
514 ansi_offset = length(replace) - 1
518 cs = colsize + ansi_offset * \
519 gsub(filter, replace, row_item[1])
520 printf "%s%-*s", quiet ? "" : "\t", cs, row_item[1]
521 for (i = 2; i <= cur_col; i++) {
522 cs = colsize + ansi_offset * \
523 gsub(filter, replace, row_item[i])
524 printf " %-*s", cs, row_item[i]
530 cur_col = ((n - 1) % ncols) + 1
531 row_item[cur_col] = $0
532 if (cur_col == ncols) print_row()
534 END { if (cur_col < ncols) print_row() }
542 echo "$*" | awk 'gsub(/'\''/, "&\\\\&&")||1'
550 die "missing profile argument (\`$pgm -Q' to list profiles)"
556 [ $COUNT -gt 0 ] && ARGV="$ARGV -N $COUNT"
557 [ "$DEBUG" ] && ARGV="$ARGV -d"
558 [ "$DESTRUCTIVE_ACTIONS" ] && ARGV="$ARGV -w"
559 [ "$EXIT_AFTER_COMPILE" ] && ARGV="$ARGV -e"
560 [ "$GROUP" ] && ARGV="$ARGV -g $GROUP"
561 [ "$JID" ] && ARGV="$ARGV -j $JID"
562 [ $MAX_ARGS -ne $_MAX_ARGS ] && ARGV="$ARGV -B $MAX_ARGS"
563 [ $MAX_DEPTH -ne $_MAX_DEPTH ] && ARGV="$ARGV -K $MAX_DEPTH"
564 [ "$ONELINE" ] && ARGV="$ARGV -1"
565 [ "$PID" ] && ARGV="$ARGV -p $PID"
566 [ "$PSTREE" ] && ARGV="$ARGV -R"
567 [ "$QUIET" ] && ARGV="$ARGV -q"
568 [ "$TIMEOUT" ] && ARGV="$ARGV -T $TIMEOUT"
569 [ "$TRACE" ] && ARGV="$ARGV -x"
570 [ "$USER" ] && ARGV="$ARGV -u $USER"
571 [ "$VERBOSE" ] && ARGV="$ARGV -v"
574 ARGV="$ARGV -r '$( shell_escape "$FILTER" )'"
576 ARGV="$ARGV -z '$( shell_escape "$EXECREGEX" )'"
577 [ "$CUSTOM_DETAILS" ] &&
578 ARGV="$ARGV -E '$( shell_escape "$EVENT_DETAILS" )'"
580 ARGV="$ARGV -t '$( shell_escape "$EVENT_TEST" )'"
582 ARGV="$ARGV -o '$( shell_escape "$OUTPUT" )'"
584 ARGV="$ARGV -O '$( shell_escape "$OUTPUT_CMD" )'"
586 case "$PROBE_TYPE" in
587 provider) ARGV="$ARGV -P" ;;
588 module) ARGV="$ARGV -m" ;;
589 function) ARGV="$ARGV -f" ;;
590 name) ARGV="$ARGV -n" ;;
594 for dir in $DWATCH_PROFILES_PATH; do
595 [ -d "$dir" ] || continue
596 [ -f "$dir/$profile" ] || continue
597 PROFILE="$profile" found=1
598 info "Sourcing $profile profile [found in %s]" "$dir"
605 die "no module named \`$profile' (\`$pgm -Q' to list profiles)"
610 local OPTIND=1 OPTARG flag
613 while getopts P: flag; do
618 shift $(( OPTIND - 1 ))
621 if [ ! "$proc" ]; then
622 if [ "$P" = "0" ]; then
623 proc="curthread->td_proc"
625 proc="this->proc ? this->proc->p_pptr : NULL"
629 awk 'NR > 1 && $0 { $0 = "\t" $0 }
630 gsub(/\\\t/, "\t") || 1
633 this->uid$P = this->proc ? this->proc->p_ucred->cr_uid : -1;
634 this->gid$P = this->proc ? this->proc->p_ucred->cr_rgid : -1;
635 this->pid$P = this->proc ? this->proc->p_pid : -1;
636 this->jid$P = this->proc ? this->proc->p_ucred->cr_prison->pr_id : -1;
638 this->p_args = this->proc ? this->proc->p_args : 0;
639 this->ar_length = this->p_args ? this->p_args->ar_length : 0;
640 this->ar_args = (char *)(this->p_args ? this->p_args->ar_args : 0);
642 this->args$P = this->arg${P}_$N = this->ar_length > 0 ?
643 \ this->ar_args : stringof(this->proc->p_comm);
644 this->len = this->ar_length > 0 ? strlen(this->ar_args) + 1 : 0;
645 this->ar_args += this->len;
646 this->ar_length -= this->len;
650 awk -v P=$P -v MAX_ARGS=$MAX_ARGS '
652 buf = buf $0 "\n" { }
654 while (++N <= MAX_ARGS) {
664 this->argP_N = this->ar_length > 0 ? this->ar_args : "";
665 this->argsP = strjoin(this->argsP,
666 \ strjoin(this->argP_N != "" ? " " : "", this->argP_N));
667 this->len = this->ar_length > 0 ? strlen(this->ar_args) + 1 : 0;
668 this->ar_args += this->len;
669 this->ar_length -= this->len;
673 N=$(( $MAX_ARGS + 1 ))
674 awk 'sub(/^\\\t/, "\t") || 1, $0 = "\t" $0' <<-EOFPROC
675 this->arg${P}_$N = this->ar_length > 0 ? "..." : "";
676 this->args$P = strjoin(this->args$P,
677 \ strjoin(this->arg${P}_$N != "" ? " " : "", this->arg${P}_$N));
683 local OPTIND=1 OPTARG flag
686 while getopts v flag; do
691 shift $(( $OPTIND - 1 ))
694 if [ "$verbose" ]; then
696 BEGIN { printf "\t" }
697 NR > 1 && $0 { $0 = "\t" $0 }
698 buf = buf $0 "\n" { }
701 if (P < 3) S = sprintf("%" 7-2*(P+1) "s", "")
703 gsub(/B/, P < 3 ? "\\" : "")
709 printf(" SB-+= %05d %d.%d %s\n",
710 \ this->pid$P, this->uid$P, this->gid$P, this->args$P);
714 printf("%s", this->args$P);
719 ############################################################ MAIN
721 # If we're running as root, no need for sudo(8)
722 [ "$( id -u )" != 0 ] && type sudo > /dev/null 2>&1 && SUDO=sudo
725 # Process command-line options
727 while getopts 1B:deE:fFg:j:k:K:lmnN:o:O:p:PqQr:Rt:T:u:vVwxX:yz: flag; do
729 1) ONELINE=1 PSTREE= ;;
730 B) MAX_ARGS="$OPTARG" ;;
732 e) EXIT_AFTER_COMPILE=1 ;;
734 EVENT_DETAILS="${EVENT_DETAILS%;}"
735 [ "$EVENT_DETAILS" ] && EVENT_DETAILS="$EVENT_DETAILS;
738 # Read event code from stdin if `-' is argument
739 [ "$OPTARG" = "-" ] && OPTARG=$( cat )
740 EVENT_DETAILS="$EVENT_DETAILS$OPTARG" ;;
741 f) PROBE_TYPE=function ;;
742 F) PROBE_COALESCE=1 ;;
743 g) GROUP="$OPTARG" ;;
745 k) EXECNAME="$EXECNAME${EXECNAME:+ }$OPTARG"
747 \**\*) name="${OPTARG%\*}"
748 predicate="strstr(execname, \"${name#\*}\") != NULL" ;;
749 \**) name="${OPTARG#\*}"
750 predicate="strstr(execname, \"$name\") == (execname +"
751 predicate="$predicate strlen(execname) - ${#name})" ;;
752 *\*) predicate="strstr(execname, \"${OPTARG%\*}\") == execname" ;;
753 *) predicate="execname == \"$OPTARG\""
755 EVENT_TEST="$predicate${EVENT_TEST:+ ||
757 K) MAX_DEPTH="$OPTARG" ;;
759 m) PROBE_TYPE=module ;;
760 n) PROBE_TYPE=name ;;
761 N) COUNT="$OPTARG" ;;
762 o) OUTPUT="$OPTARG" ;;
763 O) OUTPUT_CMD="$OPTARG" ;;
765 P) PROBE_TYPE=provider ;;
767 Q) LIST_PROFILES=1 ;;
768 r) FILTER="$OPTARG" ;;
770 t) CUSTOM_TEST="${CUSTOM_TEST:+($CUSTOM_TEST) && }$OPTARG" ;;
771 T) TIMEOUT="$OPTARG" ;;
774 V) vers="${VERSION#\$*[:\$]}"
776 printf "%s: %s\n" "$pgm" "${vers# }"
778 w) DESTRUCTIVE_ACTIONS=1 ;;
780 X) USE_PROFILE=1 PROFILE="$OPTARG" ;;
781 y) CONSOLE=1 CONSOLE_FORCE=1 ;;
782 z) EXECREGEX="$OPTARG" ;;
787 shift $(( $OPTIND - 1 ))
790 # List probes if `-l' was given
793 list_probes -f "$FILTER" ${QUIET:+-q} -t "$PROBE_TYPE" -- "$@"
797 # List profiles if `-Q' was given
799 [ "$LIST_PROFILES" ] &&
800 list_profiles ${ONELINE:+-1} -f "$FILTER" ${QUIET:+-q}
804 # Validate number of arguments
806 if [ ! "$PROFILE" ]; then
807 # If not given `-X profile' then a probe argument is required
808 [ $# -gt 0 ] || usage # NOTREACHED
812 # Validate `-N count' option argument
815 "") usage "-N option requires a number argument" ;; # NOTREACHED
816 *[!0-9]*) usage "-N argument must be a number" ;; # NOTREACHED
820 # Validate `-B num' option argument
823 "") usage "-B option requires a number argument" ;; # NOTREACHED
824 *[!0-9]*) usage "-B argument must be a number" ;; # NOTREACHED
828 # Validate `-K num' option argument
831 "") usage "-K option requires a number argument" ;; # NOTREACHED
832 *[!0-9]*) usage "-K argument must be a number" ;; # NOTREACHED
836 # Validate `-j jail' option argument
839 "") : fall through ;;
840 *[!0-9]*) JID=$( jls -j "$JID" jid ) || exit ;;
844 # Validate `-u user' option argument
847 "") : fall through ;;
848 *[![:alnum:]_-]*) RUID="$USER" ;;
849 *[!0-9]*) RUID=$( id -u "$USER" 2> /dev/null ) || die "No such user: $USER" ;;
854 # Validate `-g group' option argument
857 "") : fall-through ;;
858 *[![:alnum:]_-]*) RGID="$GROUP" ;;
860 RGID=$( getent group | awk -F: -v group="$GROUP" '
861 $1 == group { print $3; exit found=1 }
863 ' ) || die "No such group: $GROUP" ;;
868 # Expand probe argument into probe(s)
871 -*) : Assume dtrace options such as "-c cmd" or "-p pid" ;; # No probe(s) given
876 if [ "$PROBE_ARG" ]; then
879 for arg in $PROBE_ARG; do
880 arg=$( expand_probe -t "$PROBE_TYPE" -- "$arg" )
881 PROBE="$PROBE${PROBE:+, }$arg"
889 [ "$DEBUG" -a "$EXIT_AFTER_COMPILE" -a "$VERBOSE" ] && DEVELOPER=1 DEBUG=
892 # Set default event details if `-E code' was not given
894 [ "$CUSTOM_DETAILS" ] || EVENT_DETAILS=$( pproc_dump 0 )
897 # Load profile if given `-X profile'
899 [ "$USE_PROFILE" ] && load_profile "$PROFILE"
900 [ "$PROBE" ] || die "PROBE not defined by profile and none given as argument"
903 # Show the user what's being watched
905 [ "$DEBUG$EXIT_AFTER_COMPILE" ] || info "Watching '$PROBE' ..."
908 # Header for watched probe entry
911 *,*) : fall-through ;;
912 *:execve:entry|execve:entry)
913 ACTIONS=$( awk 'gsub(/\\\t/, "\t") || 1' <<-EOF
914 $PROBE /* probe ID $ID */
917 \ this->caller_execname = execname;
921 PROBE="${PROBE%entry}return"
923 EVENT_TEST="execname != this->caller_execname${EVENT_TEST:+ &&
925 EVENT_TAG='printf("%d.%d %s[%d]: ",
926 this->uid1, this->gid1, this->caller_execname, this->pid1);'
931 # Jail clause/predicate
934 prison_id="curthread->td_proc->p_ucred->cr_prison->pr_id"
935 EVENT_TEST="$prison_id == $JID${EVENT_TEST:+ &&
940 # Custom test clause/predicate
942 if [ "$CUSTOM_TEST" ]; then
943 case "$EVENT_TEST" in
944 "") EVENT_TEST="$CUSTOM_TEST" ;;
945 *) EVENT_TEST="$EVENT_TEST &&
951 # Make sure dynamic code has trailing semi-colons if non-NULL
953 EVENT_TAG="${EVENT_TAG%;}${EVENT_TAG:+;}"
954 EVENT_DETAILS="${EVENT_DETAILS%;}${EVENT_DETAILS:+;}"
959 # If `-d' is given, script is sent to stdout for debugging
960 # If `-c count", `-g group', `-r regex', or `-u user' is given, run script with
961 # dtrace and send output to awk(1) post-processor (making sure to preserve the
962 # exit code returned by dtrace invocation). Otherwise, simply run script with
963 # dtrace and then exit.
966 $PROBE /* probe ID 2 */
971 * Examine process, parent process, and grandparent process details
974 /******************* CURPROC *******************/
978 /******************* PPARENT *******************/
980 $( if [ "$PSTREE" ]; then pproc -P1; else echo -n \
981 "this->proc = this->proc ? this->proc->p_pptr : NULL;
982 this->pid1 = this->proc ? this->proc->p_pid : -1;
983 this->uid1 = this->proc ? this->proc->p_ucred->cr_uid : -1;
984 this->gid1 = this->proc ? this->proc->p_ucred->cr_rgid : -1;
985 this->jid1 = this->proc ? this->proc->p_ucred->cr_prison->pr_id : -1;"
988 /******************* GPARENT *******************/
990 $( [ "$PSTREE" ] && pproc -P2 )
992 /******************* APARENT *******************/
994 $( [ "$PSTREE" ] && pproc -P3 )
997 PSARGS_ACTION=$( cat <&9 )
998 [ "$OUTPUT" -a ! "$CONSOLE_FORCE" ] && CONSOLE=
1000 if [ "$DEBUG" ]; then
1001 # Send script to stdout
1006 if [ "$CUSTOM_TEST$EXECNAME$JID$OUTPUT$TIMEOUT$TRACE$VERBOSE" -a \
1010 [ "$CUSTOM_TEST" ] && msg="$msg test: $CUSTOM_TEST"
1011 [ "$EXECNAME" ] && msg="$msg execname: $EXECNAME"
1012 [ "$JID" ] && msg="$msg jid: $JID"
1013 [ "$OUTPUT" ] && msg="$msg output: $OUTPUT"
1014 [ "$TIMEOUT" ] && msg="$msg timeout: $TIMEOUT"
1015 [ "$TRACE" ] && msg="$msg trace: $TRACE"
1016 [ "$VERBOSE" ] && msg="$msg verbose: $VERBOSE"
1024 # Developer debugging aide
1026 if [ "$DEVELOPER" ]; then
1028 # Run, capture the error line, and focus it
1030 # Example error text to capture line number from:
1031 # dtrace: failed to compile script /dev/stdin: line 669: ...
1035 stderr_buf=$( echo "$stdin_buf" |
1036 dtrace_cmd -t -es /dev/stdin "$@" 2>&1 > /dev/null )
1038 if [ "$stderr_buf" ]; then
1039 errline=$( echo "$stderr_buf" | awk '
1045 sub(/.*: line /, "") && sub(/:.*/, "") {
1047 sub("line " $0, ti "&" te, line)
1049 { print line > "/dev/stderr" }
1052 if [ "$errline" ]; then
1053 echo "$stdin_buf" | awk -v line="${errline%%[^0-9]*}" '
1055 start = line < 10 ? 1 : line - 10
1057 slen = length(sprintf("%u", start))
1058 elen = length(sprintf("%u", end))
1059 N = elen > slen ? elen : slen
1060 for (i = start; i <= end; i++) {
1064 ti[line] = "\033[31m"
1065 te[line] = "\033[39m"
1066 fmt = "%s%*u %s%s\n"
1069 NR == start, NR == end {
1070 printf(fmt, ti[NR], N, NR, $0, te[NR])
1078 if [ $COUNT -eq 0 -a ! "$EXECREGEX$FILTER$GROUP$OUTPUT_CMD$PID$USER" ]
1081 -) output_path=/dev/stdout ;;
1082 *) output_path="$OUTPUT"
1085 # Run script without pipe to awk post-processor
1087 ${DESTRUCTIVE_ACTIONS:+-w} \
1088 ${EXIT_AFTER_COMPILE:+-e} \
1089 ${OUTPUT:+-o "$output_path"} \
1095 # Prevent backslashes from being lost
1096 FILTER=$( echo "$FILTER" | awk 'gsub(/\\/,"&&")||1' )
1097 EXECREGEX=$( echo "$EXECREGEX" | awk 'gsub(/\\/,"&&")||1' )
1099 if [ ! "$QUIET" ]; then
1101 [ "$EXECREGEX" ] && msg="$msg execregex: $EXECREGEX"
1102 [ "$FILTER" ] && msg="$msg filter: $FILTER"
1103 [ "$GROUP" ] && msg="$msg group: $GROUP"
1104 [ "$OUTPUT_CMD" ] && msg="$msg cmd: $OUTPUT_CMD"
1105 [ "$PID" ] && msg="$msg pid: $PID"
1106 [ "$USER" ] && msg="$msg user: $USER"
1107 [ $COUNT -gt 0 ] && msg="$msg count: $COUNT"
1112 # Send script output to post-processor for filtering
1117 ( exec 5>&1; to_dtrace_stderr_filter=5; (
1118 trap 'echo $? >&$to_status' EXIT
1119 eval $SUDO ${TIMEOUT:+timeout \"\$TIMEOUT\"} dtrace \
1120 ${EXIT_AFTER_COMPILE:+-e} \
1121 ${DESTRUCTIVE_ACTIONS:+-w} \
1124 2>&$to_dtrace_stderr_filter \
1125 ${QUIET:+2> /dev/null}
1127 -v cmd="$OUTPUT_CMD" \
1128 -v console="$CONSOLE" \
1130 -v execregex="$EXECREGEX" \
1131 -v filter="$FILTER" \
1133 -v output="$OUTPUT" \
1137 -v tty=$( ps -o tty= -p $$ ) \
1139 ' # Start awk(1) post-processor
1140 ############################################ BEGIN
1143 ansi = "(\\033\\[[[:digit:];]+m)?"
1144 num = year = day = "[[:digit:]]+"
1145 month = "[[:alpha:]]+"
1146 date = year " " month " +" day
1147 time = "[012][0-9]:[0-5][0-9]:[0-5][0-9]"
1148 date_time = ansi date " +" time ansi
1152 output = "/dev/stdout"
1158 execstart[1] = sprintf( \
1159 "^(%s) (%s)\\.(%s) (%s)\\[(%s)\\]: ",
1160 date_time, num, num, name1, num)
1161 execstart[2] = sprintf( \
1162 "\\n +\\\\?-\\+= (%s) (%s)\\.(%s) ",
1165 pidstart[1] = sprintf("^(%s) (%s)\\.(%s) (%s)\\[",
1166 date_time, num, num, name1)
1167 pidstart[2] = "\\n +\\\\?-\\+= "
1171 gidstart[1] = sprintf("^(%s) (%s)\\.", date_time, num)
1172 gidstart[2] = sprintf("\\n +\\\\?-\\+= (%s) (%s)\\.",
1175 uidstart[1] = sprintf("^(%s) ", date_time)
1176 uidstart[2] = sprintf("\\n +\\\\?-\\+= (%s) ",
1179 ############################################ FUNCTIONS
1180 function strip(s) { gsub(/\033\[[0-9;]*m/, "", s); return s }
1181 function esc(str) { gsub(/'\''/, "&\\\\&&", str); return str }
1182 function arg(str) { return "'\''" esc(str) "'\''" }
1183 function env(var, str) { return var "=" arg(str) " " }
1184 function ans(seq) { return console ? "\033[" seq "m" : "" }
1186 return system(sprintf("%s/bin/sh -c %s",
1187 env("TAG", strip(tag)) \
1188 env("DETAILS", strip(details)),
1191 function filter_block() {
1192 if (length(lines) < 1) return 0
1196 if (match(lines, "^(" date_time ") ")) {
1197 newstr = newstr substr(lines, 1,
1198 RSTART + RLENGTH - 1)
1199 start = RSTART + RLENGTH
1201 replace = ans("31;1") "&" ans("39;22")
1202 workstr = substr(lines, start)
1203 if (gsub(filter, replace, workstr)) block_match = 1
1204 lines = newstr workstr
1207 function filter_field(startre, fieldre, matchre, isword,
1208 preenre, defaultstr)
1210 if (length(lines) < 1) return 0
1214 while ((workstr = substr(lines, start)) &&
1215 (workstr ~ (startre fieldre)))
1217 match(workstr, startre)
1218 start += end = RSTART + RLENGTH - 1
1219 newstr = newstr substr(workstr, 1, end)
1220 workstr = substr(workstr, end + 1)
1221 match(workstr, fieldre)
1222 start += end = RSTART + RLENGTH - 1
1223 field = matchstr = substr(workstr, 1, end)
1224 sub(preenre, "", matchstr)
1225 if (!matchstr) matchstr = defaultstr
1227 if (match(matchstr, matchre) &&
1229 RLENGTH == length(matchstr)) {
1231 field = ans(7) field ans(27)
1234 replace = ans(7) "&" ans(27)
1235 if (gsub(matchre, replace, matchstr)) {
1240 newstr = newstr field
1242 lines = newstr workstr
1249 if (execregex != "") {
1250 for (n = 1; n <= nexecmatches; n++)
1251 if (filter_field(execstart[n], name2,
1252 execregex)) found = 1
1256 for (n = 1; n <= npidmatches; n++)
1257 if (filter_field(pidstart[n], num, pid,
1259 piddeflt[n])) found = 1
1263 for (n = 1; n <= ngidmatches; n++)
1264 if (filter_field(gidstart[n], num,
1265 gid, true)) found = 1
1269 for (n = 1; n <= nuidmatches; n++)
1270 if (filter_field(uidstart[n], num,
1271 uid, true)) found = 1
1274 if (filter != "" && !filter_block()) return
1279 if (!console) lines = strip(lines)
1280 print lines > output
1282 if (!quiet) print lines
1283 tag = details = lines
1284 sub(/: .*/, "", tag)
1285 sub(/.*: /, "", details)
1286 if (!console) tag = strip(tag)
1293 ############################################ MAIN
1294 { block = (block ? block "\n" : block) $0 }
1296 $0 ~ sprintf("^%6s\\\\-\\+= %s ", "", num) { dump() }
1297 count && matches >= count { exit }
1298 ############################################ END
1301 system(sprintf("pkill -t %s dtrace %s", tty,
1302 quiet ? "2> /dev/null" : ""))
1304 ' >&$console_stdout ) | dtrace_stderr_filter >&2
1309 #!/usr/sbin/dtrace -s
1311 * Copyright (c) 2014-2018 Devin Teske <dteske@FreeBSD.org>
1312 * All rights reserved.
1313 * Redistribution and use in source and binary forms, with or without
1314 * modification, are permitted provided that the following conditions
1316 * 1. Redistributions of source code must retain the above copyright
1317 * notice, this list of conditions and the following disclaimer.
1318 * 2. Redistributions in binary form must reproduce the above copyright
1319 * notice, this list of conditions and the following disclaimer in the
1320 * documentation and/or other materials provided with the distribution.
1322 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS \`\`AS IS'' AND
1323 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
1324 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
1325 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
1326 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
1327 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
1328 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
1329 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
1330 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
1331 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
1334 * $TITLE dtrace(1) script to log process(es) triggering $PROBE $
1338 $( echo "$DTRACE_PRAGMA" | awk '
1339 !/^[[:space:]]*(#|$)/, sub(/^[[:space:]]*/, "#pragma D ")||1
1344 dtrace:::BEGIN { console = ${CONSOLE:-0} } /* probe ID 1 */
1346 /*********************************************************/
1348 ${PSARGS:+$PSARGS_ACTION}
1350 /*********************************************************/
1354 /*********************************************************/
1356 $PROBE${EVENT_TEST:+ /$EVENT_TEST/} /* probe ID $ID */
1360 /***********************************************/
1363 console ? "\033[32m" : "",
1365 console ? "\033[39m" : "");
1367 /****************** EVENT_TAG ******************/
1369 ${EVENT_TAG#[[:space:]]}
1371 /**************** PROBE_COALESCE ***************/
1373 printf("%s%s:%s:%s:%s ", probename == "entry" ? "-> " :
1374 probename == "return" ? "<- " :
1375 probename == "start" ? "-> " :
1376 probename == "done" ? "<- " : " | ",
1377 probeprov, probemod, probefunc, probename);
1379 /**************** EVENT_DETAILS ****************/
1381 ${EVENT_DETAILS#[[:space:]]}
1383 /***********************************************/
1388 * Print process, parent, grandparent, and ancestor details
1399 ################################################################################
1401 ################################################################################