# psn -- Linux Process Snapper by Tanel Poder [https://0x.tools]
# Copyright 2019-2021 Tanel Poder
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
# SPDX-License-Identifier: GPL-2.0-or-later
# structures defining /proc
system_timer_hz = os.sysconf('SC_CLK_TCK')
def __init__(self, name, path, available_columns, stored_column_names, task_level=False, read_samples=lambda f: [f.read()], parse_sample=lambda self, sample: sample.split()):
self.available_columns = available_columns
self.task_level = task_level
self.read_samples = read_samples
self.parse_sample = parse_sample
self.set_stored_columns(stored_column_names)
def set_stored_columns(self, stored_column_names):
col_name_i, schema_type_i, source_i, transform_i = range(4)
self.stored_column_names = stored_column_names or [c[0] for c in self.available_columns]
sample_cols = [('event_time', str), ('pid', int), ('task', int)]
source_cols = [c for c in self.available_columns if c[col_name_i] in self.stored_column_names and c[col_name_i] not in dict(sample_cols) and c[1] is not None]
self.schema_columns = sample_cols + source_cols
column_indexes = dict([(c[col_name_i], c[source_i]) for c in self.available_columns])
schema_extract_idx = [column_indexes[c[col_name_i]] for c in source_cols]
schema_extract_convert = [c[schema_type_i] if len(c) == 3 else c[transform_i] for c in source_cols]
self.schema_extract = list(zip(schema_extract_idx, schema_extract_convert))
self.insert_sql = "INSERT INTO '%s' VALUES (%s)" % (self.name, ','.join(['?' for i in self.schema_columns]))
def sample(self, event_time, pid, task):
sample_path = self.path % (pid, task) if self.task_level else self.path % pid
with open(sample_path) as f:
raw_samples = self.read_samples(f)
def create_row_sample(raw_sample):
full_sample = self.parse_sample(self, raw_sample)
# some syscall-specific code pushed down to general sampling function
# call readlink() to get the file name for system calls that have a file descriptor as arg0
if self.name == 'syscall':
# special case: kernel threads show all-zero "syscall" on newer kernels like 4.x
# otherwise it incorrectly looks like that kernel is in a "read" syscall (id=0 on x86_64)
if full_sample[0] == '-1' or full_sample == ['0', '0x0', '0x0', '0x0', '0x0', '0x0', '0x0', '0x0', '0x0']:
full_sample = ['kernel_thread', '0x0', '0x0', '0x0', '0x0', '0x0', '0x0', '0x0', '0x0']
syscall_id = full_sample[0] # get string version of syscall number or "running" or "-1"
except (ValueError, IndexError) as e:
print('problem extracting syscall id', self.name, 'sample:')
if syscall_id in syscalls_with_fd_arg:
arg0 = int(full_sample[1], 16)
# a hacky way for avoiding reading false file descriptors for kernel threads on older kernels
# (like 2.6.32) that show "syscall 0x0" for kernel threads + some random false arguments.
# TODO refactor this and kernel_thread translation above
filename = os.readlink("/proc/%s/fd/%s" % (pid, arg0)) + " " + special_fds.get(arg0, '')
filename = 'fd over 65536'
# file has been closed or process has disappeared
#print 'problem with translating fd to name /proc/%s/fd/%s' % (pid, arg0), 'sample:'
full_sample += (filename,)
r = [event_time, pid, task] + [convert(full_sample[idx]) for idx, convert in self.schema_extract]
return [create_row_sample(rs) for rs in raw_samples]
except (ValueError, IndexError) as e:
print('problem parsing', self.name, 'sample:')
# 'R': 'Running (ON CPU)',
# 'S': 'Sleeping (Interruptible)',
# 'D': 'Disk (Uninterruptible)',
# https://github.com/torvalds/linux/blob/master/fs/proc/array.c
# State W (paging) is not used in kernels 2.6.x onwards
'R': 'Running (ON CPU)', #/* 0x00 */
'S': 'Sleep (Interruptible)', #/* 0x01 */
'D': 'Disk (Uninterruptible)', #/* 0x02 */
'T': '(stopped)', #/* 0x04 */
't': '(tracing stop)', #/* 0x08 */
'X': '(dead)', #/* 0x10 */
'Z': '(zombie)', #/* 0x20 */
'P': '(parked)', #/* 0x40 */
#/* states beyond TASK_REPORT: */
'I': '(idle)', #/* 0x80 */
def parse_stat_sample(proc_source, sample):
tokens = raw_tokens = sample.split()
# stitch together comm field of the form (word word)
if raw_tokens[1][0] == '(' and raw_tokens[1][-1] != ')':
raw_tokens = raw_tokens[2:]
while tokens[-1][-1] != ')':
tokens[-1] += ' ' + raw_tokens.pop(0)
tokens.extend(raw_tokens)
trim_comm = re.compile('\d+')
stat = ProcSource('stat', '/proc/%s/task/%s/stat', [
('comm', str, 1, lambda c: re.sub(trim_comm, '*', c)),
('state', str, 2, lambda state_id: process_state_name.get(state_id, state_id)),
('utime_sec', int, 13, lambda v: int(v) / system_timer_hz),
('stime_sec', int, 14, lambda v: int(v) / system_timer_hz),
('cutime_sec', int, 15, lambda v: int(v) / system_timer_hz),
('cstime_sec', int, 16, lambda v: int(v) / system_timer_hz),
('num_threads', int, 19),
('itrealvalue', None, 20),
('startstack', None, 27),
('exit_signal', int, 37),
('rt_priority', int, 39),
('delayacct_blkio_ticks', int, 41),
('start_data', None, 44),
parse_sample=parse_stat_sample)
def parse_status_sample(proc_source, sample):
lines = sample.split('\n')
for line in [l for l in lines if l]:
line_tokens = line.split()
n, v = line_tokens[0][:-1].lower(), ' '.join(line_tokens[1:])
# missing values take default parse function value: assume no order change, and that available_columns contains all possible field names
while len(sample_values) < len(proc_source.available_columns) and proc_source.available_columns[len(sample_values)][0] not in (n, n_kb):
parse_fn = proc_source.available_columns[len(sample_values)][1]
sample_values.append(parse_fn())
if len(sample_values) < len(proc_source.available_columns):
status = ProcSource('status', '/proc/%s/status', [
('state', str, 2), # remove duplicate with stat
('ppid', int, 6), # remove duplicate with stat
('uid', int, 8, lambda v: int(v.split()[0])),
('gid', int, 9, lambda v: int(v.split()[0])),
('vmpeak_kb', int, 16, lambda v: int(v.split()[0])),
('vmsize_kb', int, 17, lambda v: int(v.split()[0])),
('vmlck_kb', int, 18, lambda v: int(v.split()[0])),
('vmpin_kb', int, 19, lambda v: int(v.split()[0])),
('vmhwm_kb', int, 20, lambda v: int(v.split()[0])),
('vmrss_kb', int, 21, lambda v: int(v.split()[0])),
('rssanon_kb', int, 22, lambda v: int(v.split()[0])),
('rssfile_kb', int, 23, lambda v: int(v.split()[0])),
('rssshmem_kb', int, 24, lambda v: int(v.split()[0])),
('vmdata_kb', int, 25, lambda v: int(v.split()[0])),
('vmstk_kb', int, 26, lambda v: int(v.split()[0])),
('vmexe_kb', int, 27, lambda v: int(v.split()[0])),
('vmlib_kb', int, 28, lambda v: int(v.split()[0])),
('vmpte_kb', int, 29, lambda v: int(v.split()[0])),
('vmpmd_kb', int, 30, lambda v: int(v.split()[0])),
('vmswap_kb', int, 31, lambda v: int(v.split()[0])),
('hugetlbpages_kb', int, 32, lambda v: int(v.split()[0])),
('cpus_allowed', str, 46),
('cpus_allowed_list', str, 47),
('mems_allowed', str, 48),
('mems_allowed_list', str, 49),
('voluntary_ctxt_switches', int, 50),
('nonvoluntary_ctxt_switches', int, 51)
], None, task_level=False, parse_sample=parse_status_sample)
def extract_system_call_ids(unistd_64_fh):
syscall_id_to_name = {'running': '[running]', '-1': '[kernel_direct]', 'kernel_thread':'[kernel_thread]'}
# examples from a unistd.h file
# #define __NR3264_truncate 45
for name_prefix in ['__NR_', '__NR3264_']:
for line in unistd_64_fh.readlines():
if tokens and len(tokens) == 3 and tokens[0] == '#define':
if s_name.startswith(name_prefix):
s_name = s_name[len(name_prefix):]
syscall_id_to_name[s_id] = s_name
return syscall_id_to_name
# currently assuming all platforms are x86_64
def get_system_call_names():
psn_dir=os.path.dirname(os.path.realpath(__file__))
kernel_ver=platform.release().split('-')[0]
# this probably needds to be improved for better platform support
if platform.machine() == 'aarch64':
unistd_64_paths = ['/usr/include/asm-generic/unistd.h']
unistd_64_paths = ['/usr/include/asm/unistd_64.h', '/usr/include/x86_64-linux-gnu/asm/unistd_64.h', '/usr/include/asm-x86_64/unistd.h', '/usr/include/asm/unistd.h', psn_dir+'/syscall_64_'+kernel_ver+'.h', psn_dir+'/syscall_64.h']
for path in unistd_64_paths:
return extract_system_call_ids(f)
raise Exception('unistd_64.h not found in' + ' or '.join(unistd_64_paths) + '.\n You may need to "yum install kernel-headers" or "apt-get install libc6-dev"\n until this dependency is removed in a newer pSnapper version')
syscall_id_to_name = get_system_call_names()
# define syscalls for which we can look up filename from fd argument
# before the change for Python 3
#syscall_name_to_id = dict((y,x) for x,y in syscall_id_to_name.iteritems())
syscall_name_to_id = dict((y,x) for x,y in syscall_id_to_name.items())
syscalls_with_fd_arg = set([
syscall_name_to_id.get('read' , 'N/A')
, syscall_name_to_id.get('write' , 'N/A')
, syscall_name_to_id.get('pread64' , 'N/A')
, syscall_name_to_id.get('pwrite64' , 'N/A')
, syscall_name_to_id.get('fsync' , 'N/A')
, syscall_name_to_id.get('fdatasync' , 'N/A')
, syscall_name_to_id.get('recvfrom' , 'N/A')
, syscall_name_to_id.get('sendto' , 'N/A')
, syscall_name_to_id.get('recvmsg' , 'N/A')
, syscall_name_to_id.get('sendmsg' , 'N/A')
, syscall_name_to_id.get('epoll_wait' , 'N/A')
, syscall_name_to_id.get('ioctl' , 'N/A')
, syscall_name_to_id.get('accept' , 'N/A')
, syscall_name_to_id.get('accept4' , 'N/A')
, syscall_name_to_id.get('getdents' , 'N/A')
, syscall_name_to_id.get('getdents64' , 'N/A')
, syscall_name_to_id.get('unlinkat' , 'N/A')
, syscall_name_to_id.get('fstat' , 'N/A')
, syscall_name_to_id.get('fstatfs' , 'N/A')
, syscall_name_to_id.get('newfstatat' , 'N/A')
, syscall_name_to_id.get('openat' , 'N/A')
, syscall_name_to_id.get('readv' , 'N/A')
, syscall_name_to_id.get('writev' , 'N/A')
, syscall_name_to_id.get('preadv' , 'N/A')
, syscall_name_to_id.get('pwritev' , 'N/A')
, syscall_name_to_id.get('preadv2' , 'N/A')
, syscall_name_to_id.get('pwritev2' , 'N/A')
special_fds = { 0:'(stdin) ', 1:'(stdout)', 2:'(stderr)' }
def parse_syscall_sample(proc_source, sample):
if tokens[0] == 'running':
return (tokens[0], '', '', '', '', '', '', None, None)
trim_socket = re.compile('\d+')
syscall = ProcSource('syscall', '/proc/%s/task/%s/syscall', [
('syscall_id', int, 0, lambda sn: -2 if sn == 'running' else int(sn)),
('syscall', str, 0, lambda sn: syscall_id_to_name[sn]), # convert syscall_id via unistd_64.h into call name
('esp', None, 7), # stack pointer
('eip', None, 8), # program counter/instruction pointer
('filename', str, 9, lambda fn: re.sub(trim_socket, '*', fn) if fn.split(':')[0] in ['socket','pipe'] else fn),
('filenamesum',str, 9, lambda fn: re.sub(trim_socket, '*', fn)),
('basename', str, 9, lambda fn: re.sub(trim_socket, '*', fn) if fn.split(':')[0] in ['socket','pipe'] else os.path.basename(fn)), # filename if syscall has fd as arg0
('dirname', str, 9, lambda fn: re.sub(trim_socket, '*', fn) if fn.split(':')[0] in ['socket','pipe'] else os.path.dirname(fn)), # filename if syscall has fd as arg0
task_level=True, parse_sample=parse_syscall_sample)
### get file name from file descriptor ###
#filename = ProcSource('fd', '/proc/%s/task/%s/fd', [('wchan', str, 0)], ['wchan'], task_level=True)
### process cmdline args ###
def parse_cmdline_sample(proc_source,sample):
# the cmdline entry may have spaces in it and happens to have a \000 in the end
# the split [] hack is due to postgres having some extra spaces in its cmdlines
return [sample.split('\000')[0].strip()]
cmdline = ProcSource('cmdline', '/proc/%s/task/%s/cmdline', [('cmdline', str, 0)], ['cmdline'], task_level=True, parse_sample=parse_cmdline_sample)
wchan = ProcSource('wchan', '/proc/%s/task/%s/wchan', [('wchan', str, 0)], ['wchan'], task_level=True)
def parse_io_sample(proc_source, sample):
return [line.split()[1] if line else '' for line in sample.split('\n')]
io = ProcSource('io', '/proc/%s/task/%s/io', [
('cancelled_write_bytes', int, 6),
parse_sample=parse_io_sample)
### net/dev ### (not accounted at process level)
def read_net_samples(fh):
return fh.readlines()[2:]
def parse_net_sample(proc_source, sample):
fields[0] = fields[0][:-1]
net = ProcSource('net', '/proc/%s/task/%s/net/dev', [
('rx_compressed', str, 7),
('rx_multicast', str, 8),
('tx_compressed', str, 16),
read_samples=read_net_samples,
parse_sample=parse_net_sample)
def read_stack_samples(fh):
# reverse stack and ignore the (reversed) top frame 0xfffffffffffff
for x in fh.readlines()[::-1][1:]:
func = x.split(' ')[1].split('+')[0]
if func not in ['entry_SYSCALL_64_after_hwframe','do_syscall_64','el0t_64_sync_handler',
'el0_svc', 'do_el0_svc', 'el0_svc_common.constprop.0', 'invoke_syscall.constprop.0' ]:
if result: # skip writing the 1st "->"
stack = ProcSource('stack', '/proc/%s/task/%s/stack', [