1#!/usr/bin/env python 2# @lint-avoid-python-3-compatibility-imports 3# 4# ttysnoop Watch live output from a tty or pts device. 5# For Linux, uses BCC, eBPF. Embedded C. 6# 7# Due to a limited buffer size (see BUFSIZE), some commands (eg, a vim 8# session) are likely to be printed a little messed up. 9# 10# Copyright (c) 2016 Brendan Gregg. 11# Licensed under the Apache License, Version 2.0 (the "License") 12# 13# Idea: from ttywatcher. 14# 15# 15-Oct-2016 Brendan Gregg Created this. 16# 13-Dec-2022 Rong Tao Detect whether kfunc is supported. 17# 07-Jan-2023 Rong Tao Support ITER_UBUF(CO-RE way) 18 19from __future__ import print_function 20from bcc import BPF 21from subprocess import call 22import argparse 23from sys import argv 24import sys 25from os import stat 26 27def usage(): 28 print("USAGE: %s [-Ch] {PTS | /dev/ttydev} # try -h for help" % argv[0]) 29 exit() 30 31# arguments 32examples = """examples: 33 ./ttysnoop /dev/pts/2 # snoop output from /dev/pts/2 34 ./ttysnoop 2 # snoop output from /dev/pts/2 (shortcut) 35 ./ttysnoop /dev/console # snoop output from the system console 36 ./ttysnoop /dev/tty0 # snoop output from /dev/tty0 37 ./ttysnoop /dev/pts/2 -s 1024 # snoop output from /dev/pts/2 with data size 1024 38 ./ttysnoop /dev/pts/2 -c 2 # snoop output from /dev/pts/2 with 2 checks for 256 bytes of data in buffer 39 (potentially retrieving 512 bytes) 40""" 41parser = argparse.ArgumentParser( 42 description="Snoop output from a pts or tty device, eg, a shell", 43 formatter_class=argparse.RawDescriptionHelpFormatter, 44 epilog=examples) 45parser.add_argument("-C", "--noclear", action="store_true", 46 help="don't clear the screen") 47parser.add_argument("device", default="-1", 48 help="path to a tty device (eg, /dev/tty0) or pts number") 49parser.add_argument("-s", "--datasize", default="256", 50 help="size of the transmitting buffer (default 256)") 51parser.add_argument("-c", "--datacount", default="16", 52 help="number of times we check for 'data-size' data (default 16)") 53parser.add_argument("--ebpf", action="store_true", 54 help=argparse.SUPPRESS) 55args = parser.parse_args() 56debug = 0 57 58if args.device == "-1": 59 usage() 60 61path = args.device 62if path.find('/') != 0: 63 path = "/dev/pts/" + path 64try: 65 pi = stat(path) 66except: 67 print("Unable to read device %s. Exiting." % path) 68 exit() 69 70# define BPF program 71bpf_text = """ 72#include <uapi/linux/ptrace.h> 73#include <linux/fs.h> 74#include <linux/uio.h> 75 76#define BUFSIZE USER_DATASIZE 77struct data_t { 78 int count; 79 char buf[BUFSIZE]; 80}; 81 82BPF_ARRAY(data_map, struct data_t, 1); 83PERF_TABLE 84 85static int do_tty_write(void *ctx, const char __user *buf, size_t count) 86{ 87 int zero = 0, i; 88 struct data_t *data; 89 90/* We can't read data to map data before v4.11 */ 91#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 11, 0) 92 struct data_t _data = {}; 93 94 data = &_data; 95#else 96 data = data_map.lookup(&zero); 97 if (!data) 98 return 0; 99#endif 100 101 #pragma unroll 102 for (i = 0; i < USER_DATACOUNT; i++) { 103 // bpf_probe_read_user() can only use a fixed size, so truncate to count 104 // in user space: 105 if (bpf_probe_read_user(&data->buf, BUFSIZE, (void *)buf)) 106 return 0; 107 if (count > BUFSIZE) 108 data->count = BUFSIZE; 109 else 110 data->count = count; 111 PERF_OUTPUT_CTX 112 if (count < BUFSIZE) 113 return 0; 114 count -= BUFSIZE; 115 buf += BUFSIZE; 116 } 117 118 return 0; 119}; 120 121/** 122 * commit 9bb48c82aced (v5.11-rc4) tty: implement write_iter 123 * changed arguments of tty_write function 124 */ 125#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 10, 11) 126int kprobe__tty_write(struct pt_regs *ctx, struct file *file, 127 const char __user *buf, size_t count) 128{ 129 if (file->f_inode->i_ino != PTS) 130 return 0; 131 132 return do_tty_write(ctx, buf, count); 133} 134#else 135PROBE_TTY_WRITE 136{ 137 const char __user *buf = NULL; 138 const struct kvec *kvec; 139 size_t count = 0; 140 141 if (iocb->ki_filp->f_inode->i_ino != PTS) 142 return 0; 143/** 144 * commit 8cd54c1c8480 iov_iter: separate direction from flavour 145 * `type` is represented by iter_type and data_source seperately 146 */ 147#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 14, 0) 148 if (from->type != (ITER_IOVEC + WRITE)) 149 return 0; 150#else 151 if (ADD_FILTER_ITER_UBUF from->iter_type != ITER_IOVEC) 152 return 0; 153 if (from->data_source != WRITE) 154 return 0; 155#endif 156 157 /* Support 'type' and 'iter_type' field name */ 158 switch (from->IOV_ITER_TYPE_NAME) { 159 /** 160 * < 5.14.0: case ITER_IOVEC + WRITE 161 * >= 5.14.0: case ITER_IOVEC 162 */ 163 case CASE_ITER_IOVEC_NAME: 164 kvec = from->kvec; 165 buf = kvec->iov_base; 166 count = kvec->iov_len; 167 break; 168 CASE_ITER_UBUF_TEXT 169 /* TODO: Support more type */ 170 default: 171 break; 172 } 173 return do_tty_write(ctx, buf, count); 174} 175#endif 176""" 177 178probe_tty_write_kfunc = """ 179KFUNC_PROBE(tty_write, struct kiocb *iocb, struct iov_iter *from) 180""" 181 182probe_tty_write_kprobe = """ 183int kprobe__tty_write(struct pt_regs *ctx, struct kiocb *iocb, 184 struct iov_iter *from) 185""" 186 187is_support_kfunc = BPF.support_kfunc() 188if is_support_kfunc: 189 bpf_text = bpf_text.replace('PROBE_TTY_WRITE', probe_tty_write_kfunc) 190else: 191 bpf_text = bpf_text.replace('PROBE_TTY_WRITE', probe_tty_write_kprobe) 192 193if BPF.kernel_struct_has_field(b'iov_iter', b'iter_type') == 1: 194 bpf_text = bpf_text.replace('IOV_ITER_TYPE_NAME', 'iter_type') 195 bpf_text = bpf_text.replace('CASE_ITER_IOVEC_NAME', 'ITER_IOVEC') 196else: 197 bpf_text = bpf_text.replace('IOV_ITER_TYPE_NAME', 'type') 198 bpf_text = bpf_text.replace('CASE_ITER_IOVEC_NAME', 'ITER_IOVEC + WRITE') 199 200case_iter_ubuf_text = """ 201 case ITER_UBUF: 202 buf = from->ubuf; 203 count = from->count; 204 break; 205""" 206 207if BPF.kernel_struct_has_field(b'iov_iter', b'ubuf') == 1: 208 bpf_text = bpf_text.replace('CASE_ITER_UBUF_TEXT', case_iter_ubuf_text) 209 bpf_text = bpf_text.replace('ADD_FILTER_ITER_UBUF', 'from->iter_type != ITER_UBUF &&') 210else: 211 bpf_text = bpf_text.replace('CASE_ITER_UBUF_TEXT', '') 212 bpf_text = bpf_text.replace('ADD_FILTER_ITER_UBUF', '') 213 214if BPF.kernel_struct_has_field(b'bpf_ringbuf', b'waitq') == 1: 215 PERF_MODE = "USE_BPF_RING_BUF" 216 bpf_text = bpf_text.replace('PERF_TABLE', 217 'BPF_RINGBUF_OUTPUT(events, 64);') 218 bpf_text = bpf_text.replace('PERF_OUTPUT_CTX', 219 'events.ringbuf_output(data, sizeof(*data), 0);') 220else: 221 PERF_MODE = "USE_BPF_PERF_BUF" 222 bpf_text = bpf_text.replace('PERF_TABLE', 'BPF_PERF_OUTPUT(events);') 223 bpf_text = bpf_text.replace('PERF_OUTPUT_CTX', 224 'events.perf_submit(ctx, data, sizeof(*data));') 225 226bpf_text = bpf_text.replace('PTS', str(pi.st_ino)) 227if debug or args.ebpf: 228 print(bpf_text) 229 if args.ebpf: 230 exit() 231 232bpf_text = bpf_text.replace('USER_DATASIZE', '%s' % args.datasize) 233bpf_text = bpf_text.replace('USER_DATACOUNT', '%s' % args.datacount) 234 235# initialize BPF 236b = BPF(text=bpf_text) 237 238if not args.noclear: 239 call("clear") 240 241# process event 242def print_event(cpu, data, size): 243 event = b["events"].event(data) 244 print("%s" % event.buf[0:event.count].decode('utf-8', 'replace'), end="") 245 sys.stdout.flush() 246 247# loop with callback to print_event 248if PERF_MODE == "USE_BPF_RING_BUF": 249 b["events"].open_ring_buffer(print_event) 250else: 251 b["events"].open_perf_buffer(print_event, page_cnt=64) 252 253while 1: 254 try: 255 if PERF_MODE == "USE_BPF_RING_BUF": 256 b.ring_buffer_poll() 257 else: 258 b.perf_buffer_poll() 259 except KeyboardInterrupt: 260 exit() 261