#!/bin/sh #- # Copyright (c) 2014-2018 Devin Teske # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # ############################################################ IDENT(1) # # $Title: Watch processes as they trigger a particular DTrace probe $ # $FreeBSD$ # ############################################################ CONFIGURATION # # DTrace pragma settings # DTRACE_PRAGMA=" option quiet option dynvarsize=16m option switchrate=10hz " # END-QUOTE # # Profiles # : ${DWATCH_PROFILES_PATH="/usr/libexec/dwatch:/usr/local/libexec/dwatch"} ############################################################ GLOBALS VERSION='$Version: 1.4 $' # -V pgm="${0##*/}" # Program basename # # Command-line arguments # PROBE_ARG= # # Command-line defaults # _MAX_ARGS=64 # -B num _MAX_DEPTH=64 # -K num # # Command-line options # CONSOLE= # -y CONSOLE_FORCE= # -y [ -t 1 ] && CONSOLE=1 # -y COUNT=0 # -N count CUSTOM_DETAILS= # -E code CUSTOM_TEST= # -t test DEBUG= # -d DESTRUCTIVE_ACTIONS= # -w DEVELOPER= # -dev EXECNAME= # -k name EXECREGEX= # -z regex EXIT_AFTER_COMPILE= # -e FILTER= # -r regex PROBE_COALESCE= # -F GROUP= # -g group JID= # -j jail LIST= # -l LIST_PROFILES= # -Q MAX_ARGS=$_MAX_ARGS # -B num MAX_DEPTH=$_MAX_DEPTH # -K num ONELINE= # -1 OUTPUT= # -o file OUTPUT_CMD= # -O cmd PID= # -p pid PROBE_TYPE= # -f -m -n -P PROFILE= # -X profile PSTREE= # -R QUIET= # -q TIMEOUT= # -T time TRACE= # -x USER= # -u user USE_PROFILE= # -X profile VERBOSE= # -v # # Global exit status # SUCCESS=0 FAILURE=1 # # Miscellaneous # ACTIONS= EVENT_DETAILS= EVENT_TAG='printf("%d.%d %s[%d]: ", this->uid0, this->gid0, execname, this->pid0);' EVENT_TEST= FILE= ID=3 MODULE_CHECKED= PROBE= PSARGS=1 RGID= RUID= SUDO= export SUDO_PROMPT="[sudo] Password:" TITLE=\$Title: ############################################################ FUNCTIONS ansi() { local fmt="$2 $4"; [ "$CONSOLE" ] && fmt="\\033[$1m$2\\033[$3m $4"; shift 4; printf "$fmt\n" "$@"; } die() { exec >&2; [ "$*" ] && echo "$pgm:" "$@"; exit $FAILURE; } info() { [ "$QUIET" ] || ansi 35 "INFO" 39 "$@" >&2; } usage() { local optfmt="\t%-10s %s\n" exec >&2 [ "$*" ] && printf "%s: %s\n" "$pgm" "$*" printf "Usage: %s [-1defFmnPqRvVwxy] [%s] [%s] [%s] [%s]\n" "$pgm" \ "-B num" "-E code" "-g group" "-j jail" printf "\t [%s] [%s] [%s] [%s] [%s] [%s]\n" \ "-k name" "-K num" "-N count" "-o file" "-O cmd" "-p pid" printf "\t [%s] [%s] [%s] [%s] [%s] [%s]\n" \ "-r regex" "-t test" "-T time" "-u user" "-X profile" \ "-z regex" printf "\t probe[,...] [args ...]\n" printf " %s -l [-fmnPqy] [-r regex] [probe ...]\n" "$pgm" printf " %s -Q [-1qy] [-r regex]\n" "$pgm" printf "\n" printf "$optfmt" "-1" \ "Print one line per process/profile (Default; disables \`-R')." printf "$optfmt" "-B num" \ "Maximum process arguments to display (Default $_MAX_ARGS)." printf "$optfmt" "-d" \ "Debug. Send dtrace(1) script to stdout instead of executing." printf "$optfmt" "-e" \ "Exit after compiling request but prior to enabling probes." printf "$optfmt" "-E code" \ "DTrace code for event details. If \`-', read from stdin." printf "$optfmt" "-f" \ "Enable probe matching the specified function name." printf "$optfmt" "-F" \ "Coalesce trace output by function." printf "$optfmt" "-g group" \ "Group filter. Only show processes matching group name/gid." printf "$optfmt" "-j jail" \ "Jail filter. Only show processes matching jail name/jid." printf "$optfmt" "-k name" \ "Only show processes matching name." printf "$optfmt" "-K num" \ "Maximum directory depth to display (Default $_MAX_DEPTH)." printf "$optfmt" "-l" \ "List available probes on standard output and exit." printf "$optfmt" "-m" \ "Enable probe matching the specified module name." printf "$optfmt" "-n" \ "Enable probe matching the specified probe name." printf "$optfmt" "-N count" \ "Exit after count matching entries (Default 0 for disabled)." printf "$optfmt" "-o file" \ "Set output file. If \`-', the path \`/dev/stdout' is used." printf "$optfmt" "-O cmd" \ "Execute cmd for each event." printf "$optfmt" "-p pid" \ "Process id filter. Only show processes with matching pid." printf "$optfmt" "-P" \ "Enable probe matching the specified provider name." printf "$optfmt" "-q" \ "Quiet. Hide informational messages and all dtrace(1) errors." printf "$optfmt" "-Q" \ "List available profiles in DWATCH_PROFILES_PATH and exit." printf "$optfmt" "-r regex" \ "Filter. Only show blocks matching awk(1) regular expression." printf "$optfmt" "-R" \ "Show parent, grandparent, and ancestor of process." printf "$optfmt" "-t test" \ "Test clause (predicate) to limit events (Default none)." printf "$optfmt" "-T time" \ "Timeout. Format is \`\#[smhd]' or simply \`\#' for seconds." printf "$optfmt" "-u user" \ "User filter. Only show processes matching user name/uid." printf "$optfmt" "-v" \ "Verbose. Show all errors from dtrace(1)." printf "$optfmt" "-V" \ "Report dwatch version on standard output and exit." printf "$optfmt" "-w" \ "Permit destructive actions (copyout*, stop, panic, etc.)." printf "$optfmt" "-x" \ "Trace. Print \`' when a probe is triggered." printf "$optfmt" "-X profile" \ "Load profile name from DWATCH_PROFILES_PATH." printf "$optfmt" "-y" \ "Always treat stdout as console (enable colors/columns/etc.)." printf "$optfmt" "-z regex" \ "Only show processes matching awk(1) regular expression." die } dtrace_cmd() { local status stdout local timeout= if [ "$1" = "-t" ]; then shift [ "$TIMEOUT" ] && timeout=1 fi exec 3>&1 stdout=3 # # Filter dtrace(1) stderr while preserving exit status # status=$( exec 4>&1 to_status=4 ( trap 'echo $? >&$to_status' EXIT eval $SUDO ${timeout:+timeout \"\$TIMEOUT\"} dtrace \ \"\$@\" 2>&1 ${QUIET:+2> /dev/null} >&$stdout ) | dtrace_stderr_filter >&2 ) return $status } dtrace_stderr_filter() { if [ "$VERBOSE" ]; then cat return # NOTREACHED fi awk ' # Start awk(1) stderr-filter /[[:digit:]]+ drops? on CPU [[:digit:]]+/ { next } /failed to write to : No such file or directory/ { next } /failed to write to : Broken pipe/ { next } /processing aborted: Broken pipe/ { next } /invalid address \(0x[[:xdigit:]]+\) in action #[[:digit:]]+/ { next } /out of scratch space in action #[[:digit:]]+/ { next } /^Bus error$/ { next } { print; fflush() } ' # END-QUOTE } expand_probe() { local OPTIND=1 OPTARG flag local type= while getopts t: flag; do case "$flag" in t) type="$OPTARG" ;; esac done shift $(( $OPTIND - 1 )) local probe="$1" case "$probe" in *:*) echo "$probe" return $SUCCESS ;; esac dtrace_cmd -l | awk -v probe="$probe" -v type="$type" ' # Start awk(1) processor #################################################### BEGIN BEGIN { getline dtrace_header } #################################################### FUNCTIONS function dump(unused1,unused2) { if (n) { if (NcF[n] == 1) f = N2F[n] if (NcM[n] == 1) m = N2M[n] if (NcP[n] == 1) p = N2P[n] } else if (f) { if (FcM[f] == 1) m = F2M[f] if (FcP[f] == 1) p = F2P[f] if (FcN[f] == 0 && found) n = "entry" } else if (m) { if (McP[m] == 1) p = M2P[m] } printf "%s:%s:%s:%s\n", p, m, f, n exit !found } function inFMP() { return probe in F || probe in M || probe in P } function inNMP() { return probe in N || probe in M || probe in P } function inNFP() { return probe in N || probe in F || probe in P } function inNFM() { return probe in N || probe in F || probe in M } function diva(value, peerA, peerB, peerC) { return value >= peerA && value >= peerB && value >= peerC } #################################################### MAIN type == "name" && $NF != probe { next } type == "function" && NF >=4 && $(NF-1) != probe { next } type == "module" && NF == 5 && $(NF-2) != probe { next } type == "provider" && $2 != probe { next } type || $2 == probe || $3 == probe || $4 == probe || $5 == probe { P[_p = $2]++ M[_m = (NF >= 5 ? $(NF-2) : "")]++ F[_f = (NF >= 4 ? $(NF-1) : "")]++ N[_n = $NF]++ if (N2F[_n] != _f) NcF[_n]++; N2F[_n] = _f if (N2M[_n] != _m) NcM[_n]++; N2M[_n] = _m if (N2P[_n] != _p) NcP[_n]++; N2P[_n] = _p if (_n !~ /entry|return/) { if (F2N[_f] != _n) FcN[_f]++ F2N[_f] = _n } if (F2M[_f] != _m) FcM[_f]++; F2M[_f] = _m if (F2P[_f] != _p) FcP[_f]++; F2P[_f] = _p if (M2P[_m] != _p) McP[_m]++; M2P[_m] = _p } #################################################### END END { if (type == "name") dump(n = probe, found = probe in N) if (type == "function") dump(f = probe, found = probe in F) if (type == "module") dump(m = probe, found = probe in M) if (type == "provider") dump(p = probe, found = probe in P) if (probe in N) { found = 1 if (!inFMP()) dump(n = probe) if (diva(F[probe], N[probe], M[probe], P[probe])) dump(f = probe) if (diva(M[probe], N[probe], F[probe], P[probe])) dump(m = probe) if (diva(P[probe], N[probe], F[probe], M[probe])) dump(p = probe) dump(n = probe) # N is the diva } else if (probe in F) { found = 1 if (!inNMP()) dump(f = probe) if (diva(N[probe], F[probe], M[probe], P[probe])) dump(n = probe) if (diva(M[probe], F[probe], N[probe], P[probe])) dump(m = probe) if (diva(P[probe], F[probe], N[probe], M[probe])) dump(p = probe) dump(f = probe) # F is the diva } else if (probe in M) { found = 1 if (!inNFP()) dump(m = probe) if (diva(N[probe], M[probe], F[probe], P[probe])) dump(n = probe) if (diva(F[probe], M[probe], N[probe], P[probe])) dump(f = probe) if (diva(P[probe], M[probe], N[probe], F[probe])) dump(p = probe) dump(m = probe) # M is the diva } else if (probe in P) { found = 1 if (!inNFM()) dump(p = probe) if (diva(N[probe], P[probe], F[probe], M[probe])) dump(n = probe) if (diva(F[probe], P[probe], N[probe], M[probe])) dump(f = probe) if (diva(M[probe], P[probe], N[probe], F[probe])) dump(m = probe) dump(p = probe) # P is the diva } if (!found) print probe exit !found } ' # END-QUOTE } list_probes() { local OPTIND=1 OPTARG flag local column=0 header="PROVIDER:MODULE:FUNCTION:NAME" local filter= quiet= type= while getopts f:qt: flag; do case "$flag" in f) filter="$OPTARG" ;; q) quiet=1 ;; t) type="$OPTARG" ;; esac done shift $(( $OPTIND - 1 )) if [ $# -eq 0 ]; then case "$type" in provider) column=1 header="PROVIDER" ;; module) column=2 header="MODULE" ;; function) column=3 header="FUNCTION" ;; name) column=4 header="NAME" ;; esac fi [ "$quiet" ] || echo "$header" local arg probe= for arg in "$@"; do arg=$( expand_probe -t "$type" -- "$arg" ) probe="$probe${probe:+, }$arg" done dtrace_cmd -l${probe:+n "$probe"} | awk -v pattern="$( # Prevent backslashes from being lost echo "$filter" | awk 'gsub(/\\/,"&&")||1' )" -v want="$column" -v console="$CONSOLE" ' BEGIN { getline dtrace_header } function ans(seq) { return console ? "\033[" seq "m" : "" } NF > 3 && $(NF-1) ~ /^#/ { next } !_[$0 = column[0] = sprintf("%s:%s:%s:%s", column[1] = $2, column[2] = (NF >= 5 ? $(NF-2) : ""), column[3] = (NF >= 4 ? $(NF-1) : ""), column[4] = $NF)]++ && !__[$0 = column[want]]++ && gsub(pattern, ans("31;1") "&" ans("39;22")) { print | "sort" } END { close("sort") } ' # END-QUOTE exit $SUCCESS } list_profiles() { local OPTIND=1 OPTARG flag local filter= oneline= quiet= while getopts 1f:q flag; do case "$flag" in 1) oneline=1 ;; f) filter="$OPTARG" ;; q) quiet=1 ;; esac done shift $(( $OPTIND - 1 )) # Prevent backslashes from being lost filter=$( echo "$filter" | awk 'gsub(/\\/,"&&")||1' ) # Build a list of profiles available local profiles profiles=$( { IFS=: for dir in $DWATCH_PROFILES_PATH; do [ -d "$dir" ] || continue for path in $dir/*; do [ -f "$path" ] || continue name="${path##*/}" [ "$name" = "${name%%[!0-9A-Za-z_-]*}" ] || continue echo $name done done } | sort -u ) # Get the longest profile name local longest_profile_name longest_profile_name=$( echo "$profiles" | awk -v N=0 '(L = length($0)) > N { N = L } END { print N }' ) # Get the width of the terminal local max_size="$( stty size 2> /dev/null )" : ${max_size:=24 80} local max_width="${max_size#*[$IFS]}" # Determine how many columns we can display local x=$longest_profile_name ncols=1 [ "$QUIET" ] || x=$(( $x + 8 )) # Accommodate leading tab character x=$(( $x + 3 + $longest_profile_name )) # Preload end of next column while [ $x -lt $max_width ]; do ncols=$(( $ncols + 1 )) x=$(( $x + 3 + $longest_profile_name )) done # Output single lines if sent to a pipe if [ "$oneline" ]; then echo "$profiles" | awk -v filter="$filter" -v cons="$CONSOLE" ' function ans(s) { return cons ? "\033[" s "m" : "" } gsub(filter, ans("31;1") "&" ans("39;22")) ' # END-QUOTE exit $SUCCESS fi [ "$quiet" ] || echo PROFILES: echo "$profiles" | awk \ -v colsize=$longest_profile_name \ -v console="$CONSOLE" \ -v ncols=$ncols \ -v quiet="$quiet" \ -v filter="$filter" \ ' # Begin awk(1) processor function ans(seq) { return console ? "\033[" seq "m" : "" } BEGIN { row_item[1] = "" replace = ans("31;1") "&" ans("39;22") ansi_offset = length(replace) - 1 } function print_row() { cs = colsize + ansi_offset * \ gsub(filter, replace, row_item[1]) printf "%s%-*s", quiet ? "" : "\t", cs, row_item[1] for (i = 2; i <= cur_col; i++) { cs = colsize + ansi_offset * \ gsub(filter, replace, row_item[i]) printf " %-*s", cs, row_item[i] } printf "\n" } $0 ~ filter { n++ cur_col = ((n - 1) % ncols) + 1 row_item[cur_col] = $0 if (cur_col == ncols) print_row() } END { if (cur_col < ncols) print_row() } ' # END-QUOTE exit $SUCCESS } shell_escape() { echo "$*" | awk 'gsub(/'\''/, "&\\\\&&")||1' } load_profile() { local profile="$1" [ "$profile" ] || die "missing profile argument (\`$pgm -Q' to list profiles)" local oldIFS="$IFS" local dir found= local ARGV= [ $COUNT -gt 0 ] && ARGV="$ARGV -N $COUNT" [ "$DEBUG" ] && ARGV="$ARGV -d" [ "$DESTRUCTIVE_ACTIONS" ] && ARGV="$ARGV -w" [ "$EXIT_AFTER_COMPILE" ] && ARGV="$ARGV -e" [ "$GROUP" ] && ARGV="$ARGV -g $GROUP" [ "$JID" ] && ARGV="$ARGV -j $JID" [ $MAX_ARGS -ne $_MAX_ARGS ] && ARGV="$ARGV -B $MAX_ARGS" [ $MAX_DEPTH -ne $_MAX_DEPTH ] && ARGV="$ARGV -K $MAX_DEPTH" [ "$ONELINE" ] && ARGV="$ARGV -1" [ "$PID" ] && ARGV="$ARGV -p $PID" [ "$PSTREE" ] && ARGV="$ARGV -R" [ "$QUIET" ] && ARGV="$ARGV -q" [ "$TIMEOUT" ] && ARGV="$ARGV -T $TIMEOUT" [ "$TRACE" ] && ARGV="$ARGV -x" [ "$USER" ] && ARGV="$ARGV -u $USER" [ "$VERBOSE" ] && ARGV="$ARGV -v" [ "$FILTER" ] && ARGV="$ARGV -r '$( shell_escape "$FILTER" )'" [ "$EXECREGEX" ] && ARGV="$ARGV -z '$( shell_escape "$EXECREGEX" )'" [ "$CUSTOM_DETAILS" ] && ARGV="$ARGV -E '$( shell_escape "$EVENT_DETAILS" )'" [ "$CUSTOM_TEST" ] && ARGV="$ARGV -t '$( shell_escape "$CUSTOM_TEST" )'" [ "$OUTPUT" ] && ARGV="$ARGV -o '$( shell_escape "$OUTPUT" )'" [ "$OUTPUT_CMD" ] && ARGV="$ARGV -O '$( shell_escape "$OUTPUT_CMD" )'" case "$PROBE_TYPE" in provider) ARGV="$ARGV -P" ;; module) ARGV="$ARGV -m" ;; function) ARGV="$ARGV -f" ;; name) ARGV="$ARGV -n" ;; esac IFS=: for dir in $DWATCH_PROFILES_PATH; do [ -d "$dir" ] || continue [ -f "$dir/$profile" ] || continue PROFILE="$profile" found=1 info "Sourcing $profile profile [found in %s]" "$dir" . "$dir/$profile" break done IFS="$oldIFS" [ "$found" ] || die "no module named \`$profile' (\`$pgm -Q' to list profiles)" } pproc() { local OPTIND=1 OPTARG flag local P= N=0 while getopts P: flag; do case "$flag" in P) P="$OPTARG" ;; esac done shift $(( OPTIND - 1 )) local proc=$1 if [ ! "$proc" ]; then if [ "$P" = "0" ]; then proc="curthread->td_proc" else proc="this->proc ? this->proc->p_pptr : NULL" fi fi awk 'NR > 1 && $0 { $0 = "\t" $0 } gsub(/\\\t/, "\t") || 1 ' <<-EOFPREAMBLE this->proc = $proc; this->uid$P = this->proc ? this->proc->p_ucred->cr_uid : -1; this->gid$P = this->proc ? this->proc->p_ucred->cr_rgid : -1; this->pid$P = this->proc ? this->proc->p_pid : -1; this->jid$P = this->proc ? this->proc->p_ucred->cr_prison->pr_id : -1; this->p_args = this->proc ? this->proc->p_args : 0; this->ar_length = this->p_args ? this->p_args->ar_length : 0; this->ar_args = (char *)(this->p_args ? this->p_args->ar_args : 0); this->args$P = this->arg${P}_$N = this->ar_length > 0 ? \ this->ar_args : stringof(this->proc->p_comm); this->len = this->ar_length > 0 ? strlen(this->ar_args) + 1 : 0; this->ar_args += this->len; this->ar_length -= this->len; EOFPREAMBLE awk -v P=$P -v MAX_ARGS=$MAX_ARGS ' $0 { $0 = "\t" $0 } buf = buf $0 "\n" { } END { while (++N <= MAX_ARGS) { $0 = buf gsub(/P/, P) gsub(/N/, N) gsub(/\\\t/, "\t") sub(/\n$/, "") print } } ' <<-EOFARGS this->argP_N = this->ar_length > 0 ? this->ar_args : ""; this->argsP = strjoin(this->argsP, \ strjoin(this->argP_N != "" ? " " : "", this->argP_N)); this->len = this->ar_length > 0 ? strlen(this->ar_args) + 1 : 0; this->ar_args += this->len; this->ar_length -= this->len; EOFARGS N=$(( $MAX_ARGS + 1 )) awk 'sub(/^\\\t/, "\t") || 1, $0 = "\t" $0' <<-EOFPROC this->arg${P}_$N = this->ar_length > 0 ? "..." : ""; this->args$P = strjoin(this->args$P, \ strjoin(this->arg${P}_$N != "" ? " " : "", this->arg${P}_$N)); EOFPROC } pproc_dump() { local OPTIND=1 OPTARG flag local verbose= while getopts v flag; do case "$flag" in v) verbose=1 ;; esac done shift $(( $OPTIND - 1 )) local P=$1 if [ "$verbose" ]; then awk -v P=$P ' BEGIN { printf "\t" } NR > 1 && $0 { $0 = "\t" $0 } buf = buf $0 "\n" { } END { $0 = buf if (P < 3) S = sprintf("%" 7-2*(P+1) "s", "") gsub(/S/, S) gsub(/B/, P < 3 ? "\\" : "") gsub(/\\\t/, "\t") sub(/\n$/, "") print } ' <<-EOFPREAMBLE printf(" SB-+= %05d %d.%d %s\n", \ this->pid$P, this->uid$P, this->gid$P, this->args$P); EOFPREAMBLE else cat <<-EOFPREAMBLE printf("%s", this->args$P); EOFPREAMBLE fi } ############################################################ MAIN # If we're running as root, no need for sudo(8) [ "$( id -u )" != 0 ] && type sudo > /dev/null 2>&1 && SUDO=sudo # # Process command-line options # while getopts 1B:deE:fFg:j:k:K:lmnN:o:O:p:PqQr:Rt:T:u:vVwxX:yz: flag; do case "$flag" in 1) ONELINE=1 PSTREE= ;; B) MAX_ARGS="$OPTARG" ;; d) DEBUG=1 ;; e) EXIT_AFTER_COMPILE=1 ;; E) CUSTOM_DETAILS=1 EVENT_DETAILS="${EVENT_DETAILS%;}" [ "$EVENT_DETAILS" ] && EVENT_DETAILS="$EVENT_DETAILS; printf(\" \"); " # END-QUOTE # Read event code from stdin if `-' is argument [ "$OPTARG" = "-" ] && OPTARG=$( cat ) EVENT_DETAILS="$EVENT_DETAILS$OPTARG" ;; f) PROBE_TYPE=function ;; F) PROBE_COALESCE=1 ;; g) GROUP="$OPTARG" ;; j) JID="$OPTARG" ;; k) EXECNAME="$EXECNAME${EXECNAME:+ }$OPTARG" case "$OPTARG" in \**\*) name="${OPTARG%\*}" predicate="strstr(execname, \"${name#\*}\") != NULL" ;; \**) name="${OPTARG#\*}" predicate="strstr(execname, \"$name\") == (execname +" predicate="$predicate strlen(execname) - ${#name})" ;; *\*) predicate="strstr(execname, \"${OPTARG%\*}\") == execname" ;; *) predicate="execname == \"$OPTARG\"" esac EVENT_TEST="$predicate${EVENT_TEST:+ || ($EVENT_TEST)}" ;; K) MAX_DEPTH="$OPTARG" ;; l) LIST=1 ;; m) PROBE_TYPE=module ;; n) PROBE_TYPE=name ;; N) COUNT="$OPTARG" ;; o) OUTPUT="$OPTARG" ;; O) OUTPUT_CMD="$OPTARG" ;; p) PID="$OPTARG" ;; P) PROBE_TYPE=provider ;; q) QUIET=1 ;; Q) LIST_PROFILES=1 ;; r) FILTER="$OPTARG" ;; R) PSTREE=1 ;; t) CUSTOM_TEST="${CUSTOM_TEST:+($CUSTOM_TEST) && }$OPTARG" ;; T) TIMEOUT="$OPTARG" ;; u) USER="$OPTARG" ;; v) VERBOSE=1 ;; V) vers="${VERSION#\$*[:\$]}" vers="${vers% \$}" printf "%s: %s\n" "$pgm" "${vers# }" exit ;; w) DESTRUCTIVE_ACTIONS=1 ;; x) TRACE=1 ;; X) USE_PROFILE=1 PROFILE="$OPTARG" ;; y) CONSOLE=1 CONSOLE_FORCE=1 ;; z) EXECREGEX="$OPTARG" ;; *) usage # NOTREACHED esac done shift $(( $OPTIND - 1 )) # # List probes if `-l' was given # [ "$LIST" ] && list_probes -f "$FILTER" ${QUIET:+-q} -t "$PROBE_TYPE" -- "$@" # NOTREACHED # # List profiles if `-Q' was given # [ "$LIST_PROFILES" ] && list_profiles ${ONELINE:+-1} -f "$FILTER" ${QUIET:+-q} # NOTREACHED # # Validate number of arguments # if [ ! "$PROFILE" ]; then # If not given `-X profile' then a probe argument is required [ $# -gt 0 ] || usage # NOTREACHED fi # # Validate `-N count' option argument # case "$COUNT" in "") usage "-N option requires a number argument" ;; # NOTREACHED *[!0-9]*) usage "-N argument must be a number" ;; # NOTREACHED esac # # Validate `-B num' option argument # case "$MAX_ARGS" in "") usage "-B option requires a number argument" ;; # NOTREACHED *[!0-9]*) usage "-B argument must be a number" ;; # NOTREACHED esac # # Validate `-K num' option argument # case "$MAX_DEPTH" in "") usage "-K option requires a number argument" ;; # NOTREACHED *[!0-9]*) usage "-K argument must be a number" ;; # NOTREACHED esac # # Validate `-j jail' option argument # case "$JID" in "") : fall through ;; *[!0-9]*) JID=$( jls -j "$JID" jid ) || exit ;; esac # # Validate `-u user' option argument # case "$USER" in "") : fall through ;; *[![:alnum:]_-]*) RUID="$USER" ;; *[!0-9]*) RUID=$( id -u "$USER" 2> /dev/null ) || die "No such user: $USER" ;; *) RUID=$USER esac # # Validate `-g group' option argument # case "$GROUP" in "") : fall-through ;; *[![:alnum:]_-]*) RGID="$GROUP" ;; *[!0-9]*) RGID=$( getent group | awk -F: -v group="$GROUP" ' $1 == group { print $3; exit found=1 } END { exit !found } ' ) || die "No such group: $GROUP" ;; *) RGID=$GROUP esac # # Expand probe argument into probe(s) # case "$1" in -*) : Assume dtrace options such as "-c cmd" or "-p pid" ;; # No probe(s) given *) PROBE_ARG="$1" shift esac if [ "$PROBE_ARG" ]; then oldIFS="$IFS" IFS="$IFS," for arg in $PROBE_ARG; do arg=$( expand_probe -t "$PROBE_TYPE" -- "$arg" ) PROBE="$PROBE${PROBE:+, }$arg" done IFS="$oldIFS" fi # # Developer switch # [ "$DEBUG" -a "$EXIT_AFTER_COMPILE" -a "$VERBOSE" ] && DEVELOPER=1 DEBUG= # # Set default event details if `-E code' was not given # [ "$CUSTOM_DETAILS" ] || EVENT_DETAILS=$( pproc_dump 0 ) # # Load profile if given `-X profile' # [ "$USE_PROFILE" ] && load_profile "$PROFILE" [ "$PROBE" ] || die "PROBE not defined by profile and none given as argument" # # Show the user what's being watched # [ "$DEBUG$EXIT_AFTER_COMPILE" ] || info "Watching '$PROBE' ..." # # Header for watched probe entry # case "$PROBE" in *,*) : fall-through ;; *:execve:entry|execve:entry) ACTIONS=$( awk 'gsub(/\\\t/, "\t") || 1' <<-EOF $PROBE /* probe ID $ID */ {${TRACE:+ \ printf("<$ID>");} \ this->caller_execname = execname; } EOF ) PROBE="${PROBE%entry}return" ID=$(( $ID + 1 )) EVENT_TEST="execname != this->caller_execname${EVENT_TEST:+ && ($EVENT_TEST)}" EVENT_TAG='printf("%d.%d %s[%d]: ", this->uid1, this->gid1, this->caller_execname, this->pid1);' ;; esac # # Jail clause/predicate # if [ "$JID" ]; then prison_id="curthread->td_proc->p_ucred->cr_prison->pr_id" EVENT_TEST="$prison_id == $JID${EVENT_TEST:+ && ($EVENT_TEST)}" fi # # Custom test clause/predicate # if [ "$CUSTOM_TEST" ]; then case "$EVENT_TEST" in "") EVENT_TEST="$CUSTOM_TEST" ;; *) EVENT_TEST="$EVENT_TEST && ($CUSTOM_TEST)" esac fi # # Make sure dynamic code has trailing semi-colons if non-NULL # EVENT_TAG="${EVENT_TAG%;}${EVENT_TAG:+;}" EVENT_DETAILS="${EVENT_DETAILS%;}${EVENT_DETAILS:+;}" # # DTrace script # # If `-d' is given, script is sent to stdout for debugging # If `-c count", `-g group', `-r regex', or `-u user' is given, run script with # dtrace and send output to awk(1) post-processor (making sure to preserve the # exit code returned by dtrace invocation). Otherwise, simply run script with # dtrace and then exit. # exec 9<"); } /* * Examine process, parent process, and grandparent process details */ /******************* CURPROC *******************/ $( pproc -P0 ) /******************* PPARENT *******************/ $( if [ "$PSTREE" ]; then pproc -P1; else echo -n \ "this->proc = this->proc ? this->proc->p_pptr : NULL; this->pid1 = this->proc ? this->proc->p_pid : -1; this->uid1 = this->proc ? this->proc->p_ucred->cr_uid : -1; this->gid1 = this->proc ? this->proc->p_ucred->cr_rgid : -1; this->jid1 = this->proc ? this->proc->p_ucred->cr_prison->pr_id : -1;" fi ) /******************* GPARENT *******************/ $( [ "$PSTREE" ] && pproc -P2 ) /******************* APARENT *******************/ $( [ "$PSTREE" ] && pproc -P3 ) } EOF PSARGS_ACTION=$( cat <&9 ) [ "$OUTPUT" -a ! "$CONSOLE_FORCE" ] && CONSOLE= { if [ "$DEBUG" ]; then # Send script to stdout cat exit fi if [ "$CUSTOM_TEST$EXECNAME$JID$OUTPUT$TIMEOUT$TRACE$VERBOSE" -a \ ! "$QUIET" ] then msg=Setting [ "$CUSTOM_TEST" ] && msg="$msg test: $CUSTOM_TEST" [ "$EXECNAME" ] && msg="$msg execname: $EXECNAME" [ "$JID" ] && msg="$msg jid: $JID" [ "$OUTPUT" ] && msg="$msg output: $OUTPUT" [ "$TIMEOUT" ] && msg="$msg timeout: $TIMEOUT" [ "$TRACE" ] && msg="$msg trace: $TRACE" [ "$VERBOSE" ] && msg="$msg verbose: $VERBOSE" info "$msg" fi exec 3>&1 console_stdout=3 # # Developer debugging aide # if [ "$DEVELOPER" ]; then # # Run, capture the error line, and focus it # # Example error text to capture line number from: # dtrace: failed to compile script /dev/stdin: line 669: ... # errline= stdin_buf=$( cat ) stderr_buf=$( echo "$stdin_buf" | dtrace_cmd -t -es /dev/stdin "$@" 2>&1 > /dev/null ) status=$? if [ "$stderr_buf" ]; then errline=$( echo "$stderr_buf" | awk ' BEGIN { ti = "\033[31m" te = "\033[39m" } { line = $0 } sub(/.*: line /, "") && sub(/:.*/, "") { print # to errline sub("line " $0, ti "&" te, line) } { print line > "/dev/stderr" } ' 2>&3 ) fi if [ "$errline" ]; then echo "$stdin_buf" | awk -v line="${errline%%[^0-9]*}" ' BEGIN { start = line < 10 ? 1 : line - 10 end = line + 10 slen = length(sprintf("%u", start)) elen = length(sprintf("%u", end)) N = elen > slen ? elen : slen ti[line] = "\033[31m" te[line] = "\033[39m" fmt = "%s%*u %s%s\n" } NR < start { next } NR == start, NR == end { printf(fmt, ti[NR], N, NR, $0, te[NR]) } NR > end { exit } ' # END-QUOTE fi exit $status fi if [ $COUNT -eq 0 -a ! "$EXECREGEX$FILTER$GROUP$OUTPUT_CMD$PID$USER" ] then case "$OUTPUT" in -) output_path=/dev/stdout ;; *) output_path="$OUTPUT" esac # Run script without pipe to awk post-processor dtrace_cmd -t \ ${DESTRUCTIVE_ACTIONS:+-w} \ ${EXIT_AFTER_COMPILE:+-e} \ ${OUTPUT:+-o "$output_path"} \ -s /dev/stdin \ "$@" exit fi # Prevent backslashes from being lost FILTER=$( echo "$FILTER" | awk 'gsub(/\\/,"&&")||1' ) EXECREGEX=$( echo "$EXECREGEX" | awk 'gsub(/\\/,"&&")||1' ) if [ ! "$QUIET" ]; then msg=Filtering [ "$EXECREGEX" ] && msg="$msg execregex: $EXECREGEX" [ "$FILTER" ] && msg="$msg filter: $FILTER" [ "$GROUP" ] && msg="$msg group: $GROUP" [ "$OUTPUT_CMD" ] && msg="$msg cmd: $OUTPUT_CMD" [ "$PID" ] && msg="$msg pid: $PID" [ "$USER" ] && msg="$msg user: $USER" [ $COUNT -gt 0 ] && msg="$msg count: $COUNT" info "$msg" fi # # Send script output to post-processor for filtering # status=$( exec 4>&1 to_status=4 ( exec 5>&1; to_dtrace_stderr_filter=5; ( trap 'echo $? >&$to_status' EXIT eval $SUDO ${TIMEOUT:+timeout \"\$TIMEOUT\"} dtrace \ ${EXIT_AFTER_COMPILE:+-e} \ ${DESTRUCTIVE_ACTIONS:+-w} \ -s /dev/stdin \ \"\$@\" \ 2>&$to_dtrace_stderr_filter \ ${QUIET:+2> /dev/null} ) | $SUDO awk \ -v cmd="$OUTPUT_CMD" \ -v console="$CONSOLE" \ -v count=$COUNT \ -v execregex="$EXECREGEX" \ -v filter="$FILTER" \ -v gid="$RGID" \ -v output="$OUTPUT" \ -v pid="$PID" \ -v pstree=$PSTREE \ -v quiet=$QUIET \ -v tty=$( ps -o tty= -p $$ ) \ -v uid="$RUID" \ ' # Start awk(1) post-processor ############################################ BEGIN BEGIN { true = 1 ansi = "(\\033\\[[[:digit:];]+m)?" num = year = day = "[[:digit:]]+" month = "[[:alpha:]]+" date = year " " month " +" day time = "[012][0-9]:[0-5][0-9]:[0-5][0-9]" date_time = ansi date " +" time ansi name1 = "[^\\[]*" name2 = "[^\\n]*" if (output == "-") output = "/dev/stdout" # # Field definitions # nexecmatches = 2 execstart[1] = sprintf( \ "^(%s) (%s)\\.(%s) (%s)\\[(%s)\\]: ", date_time, num, num, name1, num) execstart[2] = sprintf( \ "\\n +\\\\?-\\+= (%s) (%s)\\.(%s) ", num, num, num) npidmatches = 2 pidstart[1] = sprintf("^(%s) (%s)\\.(%s) (%s)\\[", date_time, num, num, name1) pidstart[2] = "\\n +\\\\?-\\+= " pidpreen[2] = "^0*" piddeflt[2] = "0" ngidmatches = 2 gidstart[1] = sprintf("^(%s) (%s)\\.", date_time, num) gidstart[2] = sprintf("\\n +\\\\?-\\+= (%s) (%s)\\.", ansi num ansi, num) nuidmatches = 2 uidstart[1] = sprintf("^(%s) ", date_time) uidstart[2] = sprintf("\\n +\\\\?-\\+= (%s) ", ansi num ansi) } ############################################ FUNCTIONS function strip(s) { gsub(/\033\[[0-9;]*m/, "", s); return s } function esc(str) { gsub(/'\''/, "&\\\\&&", str); return str } function arg(str) { return "'\''" esc(str) "'\''" } function env(var, str) { return var "=" arg(str) " " } function ans(seq) { return console ? "\033[" seq "m" : "" } function runcmd() { return system(sprintf("%s/bin/sh -c %s", env("TAG", strip(tag)) \ env("DETAILS", strip(details)), arg(cmd))) } function filter_block() { if (length(lines) < 1) return 0 block_match = 0 newstr = "" start = 1 if (match(lines, "^(" date_time ") ")) { newstr = newstr substr(lines, 1, RSTART + RLENGTH - 1) start = RSTART + RLENGTH } replace = ans("31;1") "&" ans("39;22") workstr = substr(lines, start) if (gsub(filter, replace, workstr)) block_match = 1 lines = newstr workstr return block_match } function filter_field(startre, fieldre, matchre, isword, preenre, defaultstr) { if (length(lines) < 1) return 0 field_match = 0 newstr = "" start = 1 while ((workstr = substr(lines, start)) && (workstr ~ (startre fieldre))) { match(workstr, startre) start += end = RSTART + RLENGTH - 1 newstr = newstr substr(workstr, 1, end) workstr = substr(workstr, end + 1) match(workstr, fieldre) start += end = RSTART + RLENGTH - 1 field = matchstr = substr(workstr, 1, end) sub(preenre, "", matchstr) if (!matchstr) matchstr = defaultstr if (isword) { if (match(matchstr, matchre) && RSTART == 1 && RLENGTH == length(matchstr)) { field_match = 1 field = ans(7) field ans(27) } } else { replace = ans(7) "&" ans(27) if (gsub(matchre, replace, matchstr)) { field_match = 1 field = matchstr } } newstr = newstr field } lines = newstr workstr return field_match } function dump() { lines = block block = "" found = 0 if (execregex != "") { for (n = 1; n <= nexecmatches; n++) if (filter_field(execstart[n], name2, execregex)) found = 1 if (!found) return } if (pid != "") { for (n = 1; n <= npidmatches; n++) if (filter_field(pidstart[n], num, pid, true, pidpreen[n], piddeflt[n])) found = 1 if (!found) return } if (gid != "") { for (n = 1; n <= ngidmatches; n++) if (filter_field(gidstart[n], num, gid, true)) found = 1 if (!found) return } if (uid != "") { for (n = 1; n <= nuidmatches; n++) if (filter_field(uidstart[n], num, uid, true)) found = 1 if (!found) return } if (filter != "" && !filter_block()) return if (lines) { stdout = 1 if (output) { stdout = 0 if (!console) lines = strip(lines) print lines > output } else if (cmd) { if (!quiet) print lines tag = details = lines sub(/: .*/, "", tag) sub(/.*: /, "", details) if (!console) tag = strip(tag) runcmd() } else print lines } fflush() ++matches } ############################################ MAIN { block = (block ? block "\n" : block) $0 } !pstree { dump() } $0 ~ sprintf("^%6s\\\\-\\+= %s ", "", num) { dump() } count && matches >= count { exit } ############################################ END END { dump() system(sprintf("pkill -t %s dtrace %s", tty, quiet ? "2> /dev/null" : "")) } ' >&$console_stdout ) | dtrace_stderr_filter >&2 ) # status exit $status } < * All rights reserved. * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS \`\`AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * $TITLE dtrace(1) script to log process(es) triggering $PROBE $ * \$FreeBSD$ */ $( echo "$DTRACE_PRAGMA" | awk ' !/^[[:space:]]*(#|$)/, sub(/^[[:space:]]*/, "#pragma D ")||1 ' ) int console; dtrace:::BEGIN { console = ${CONSOLE:-0} } /* probe ID 1 */ /*********************************************************/ ${PSARGS:+$PSARGS_ACTION} ${ACTIONS:+ /*********************************************************/ $ACTIONS } /*********************************************************/ $PROBE${EVENT_TEST:+ / $EVENT_TEST /} /* probe ID $ID */ {${TRACE:+ printf("<$ID>"); } /***********************************************/ printf("%s%Y%s ", console ? "\033[32m" : "", walltimestamp, console ? "\033[39m" : ""); /****************** EVENT_TAG ******************/ ${EVENT_TAG#[[:space:]]} ${PROBE_COALESCE:+ /**************** PROBE_COALESCE ***************/ printf("%s%s:%s:%s:%s ", probename == "entry" ? "-> " : probename == "return" ? "<- " : probename == "start" ? "-> " : probename == "done" ? "<- " : " | ", probeprov, probemod, probefunc, probename); } /**************** EVENT_DETAILS ****************/ ${EVENT_DETAILS#[[:space:]]} /***********************************************/ printf("\\n"); ${PSTREE:+ /* * Print process, parent, grandparent, and ancestor details */ $( pproc_dump -v 3 pproc_dump -v 2 pproc_dump -v 1 pproc_dump -v 0 )} } EOF # NOTREACHED ################################################################################ # END ################################################################################