Skip to content

Instantly share code, notes, and snippets.

@jirutka
Last active November 30, 2022 15:11
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jirutka/9f7624a54b6f44b31c1dce3bea11708f to your computer and use it in GitHub Desktop.
Save jirutka/9f7624a54b6f44b31c1dce3bea11708f to your computer and use it in GitHub Desktop.
Find processes that use (maps into memory) files which have been deleted or replaced on disk.
#!/bin/sh
#---help---
# Usage: procs-using-deleted-files [options]
#
# Find processes that use (maps into memory) files which have been deleted
# or replaced on disk. If /proc/$PID/map_files is accessible, then it omits
# files that have been replaced by the same files (i.e. content is the same).
#
# Options:
# -e PATT... Case pattern (POSIX shell's "case") specifying paths to exclude
# when looking for modified mapped files.
# -H Print heading.
# -v... Verbosity (may be repeated up to 3 times).
# -h Show this message and exit.
#
# Source: <https://gist.github.com/jirutka/9f7624a54b6f44b31c1dce3bea11708f>
#---help---
set -eu
# Set pipefail if supported.
if ( set -o pipefail 2>/dev/null ); then
set -o pipefail
fi
PROGNAME='procs-using-deleted-files'
help() {
sed -En '/^#---help---/,/^#---help---/p' "$0" | sed -E 's/^# ?//; 1d;$d;'
exit ${1:-0}
}
# Returns 0 if $1 matches "case" pattern $2, otherwise returns 1.
case_match() {
[ "$2" ] || return 1
eval "case \"$1\" in $2) return 0;; *) return 1;; esac"
}
# Prints paths of files mapped by the process with PID $1 that have been
# deleted or replaced. You may specify "case" pattern $2 to exclude certain
# paths (e.g. "/dev/*|/home/*"). Returns 1 if no files was found.
changed_map_files() {
local pid="$1"
local exclude="${2:-}"
if [ ! -r /proc/$pid/maps ]; then
echo "$PROGNAME: could not read /proc/$pid/maps" >&2
return 2
fi
cat /proc/$pid/maps \
| sed -En 's|^([a-f0-9-]+) .* {4,}(/.*) \(deleted\)$|\1 \2|p' \
| grep -v '[[:cntrl:]"$\`]' \
| sort -k 2 \
| uniq -f 1 \
| while read maddr filepath; do
map_file="/proc/$pid/map_files/$maddr"
# Skip excluded path.
case_match "$filepath" "$exclude" && continue
# Skip path that is not a file.
[ -e "$filepath" ] && [ -f "$filepath" ] || continue
# Don't compare file's content if /proc/$pid/map_files
# is not accessible (i.e. on Grsecurity kernel).
if [ -x "${map_file%/*}" ]; then
[ -e "$map_file" ] || continue
# Skip file which content has not been changed.
[ -e "$filepath" ] \
&& ! cmp -s "$map_file" "$filepath" 2>/dev/null \
|| continue
fi
echo "$filepath"
done | grep .
}
# Prints executable of the process with PID $1.
proc_exe() {
local exe
exe=$(readlink /proc/$1/exe)
exe=${exe% (deleted)}
exe=${exe%.apk-new} # special case for apk (Alpine Linux)
printf '%s\n' "$exe"
}
exclude_maps=''
print_header='no'
verbosity=0
while getopts ':e:Hhv' OPTION; do
case "$OPTION" in
H) print_header='yes';;
e) exclude_maps="${exclude_maps:+"$exclude_maps|"}$OPTARG";;
v) verbosity=$(( $verbosity + 1 ));;
h) echo "Usage: $0 [-H] [-v] [-h]" >&2; exit 0;;
\?) echo "$PROGNAME: unrecognized option -$OPTARG" >&2; exit 100;;
esac
done
# Sanitize exclude pattern.
if [ "$exclude_maps" ]; then
exclude_maps=$(printf %s "$exclude_maps" | sed 's|[();]|\\&|g')
fi
if [ "$print_header" = yes ]; then
printf 'PID'
[ $verbosity -ge 1 ] && printf '\tEXE\tCMDLINE'
[ $verbosity -ge 2 ] && printf '\tMAPPED FILES'
printf '\n'
fi
for proc in /proc/[0-9]*; do
pid=${proc#/proc/}
# Skip kernel threads.
[ -e $proc/exe ] || continue
if files=$(changed_map_files $pid "$exclude_maps"); then
if [ $verbosity -ge 0 ]; then
printf '%d' $pid
fi
if [ $verbosity -ge 1 ]; then
printf '\t%s\t%s' \
"$(proc_exe $pid)" \
"$(cat $proc/cmdline | tr '\0\t' ' ')"
fi
if [ $verbosity -ge 2 ]; then
printf '\t'
printf '%s ' $files
fi
printf '\n'
fi
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment