我还是要赞美 @LeonHwang (gh: Asphaltt) 的 bpfsnoop。
我仔细 review 哪一步是运气,意识到 retsnoop 这一步就是狗屎运:
# sudo ./retsnoop -x EIO -a 'tty' -e 'tty'
10:10:44.178847 -> 10:10:44.178850 TID/PID 30657/30657 (python/python):
entry_SYSCALL_64_after_hwframe+0x76
do_syscall_64+0x7e
x64_sys_call+0x131e
__x64_sys_ioctl+0xa4
2us [-EIO] tty_ioctl+0x515
⌇ 1us [-EIO] n_tty_ioctl+0x85
⌇ 1us [-EIO] n_tty_ioctl_helper+0x2d
⌇ 0us [-EIO] tty_mode_ioctl+0x1c1
set_termios+0x5d
⌇ 0us [-EIO] tty_check_change+0x13
!⌇ 0us [-EIO] __tty_check_change
halfling 的好运,返回 EIO 的内核函数名正好在 tty pattern 里,所以直接过滤出来了。但凡运气平凡一点,恐怕还要在摸索个半小时的内核才能找到返回 EIO 的地方。
这其实是个一般化的难题:已知用户态进程调用 syscall 报错,问内核为什么报错了。
retsnoop 已经算是很好的工具了,但还不够好,我仔细想了一会儿,意识到在 tracepoint 出口做 lbr trace 就是我想要的,而这 bpfsnoop 已经实现了。
预备,开始
$ sudo bpfsnoop -k __x64_sys_ioctl --output-lbr --filter-pid $(pidof python) --filter-arg 'retval==-5'
LBR stack:
[#31] is_current_pgrp_orphaned+0x4c -> __tty_check_change+0xd2
__tty_check_change+0xd6 -> __rcu_read_unlock+0x0
__rcu_read_unlock+0x24 -> __tty_check_change+0xdb
__tty_check_change+0xe0 -> __tty_check_change+0x30
__tty_check_change+0x46 -> tty_check_change+0x13
tty_check_change+0x18 -> set_termios+0x5d
set_termios+0x90 -> tty_mode_ioctl+0x1c1
tty_mode_ioctl+0x1c1 -> tty_mode_ioctl+0xc4
tty_mode_ioctl+0xf0 -> n_tty_ioctl_helper+0x2d
n_tty_ioctl_helper+0x3d -> n_tty_ioctl+0x85
n_tty_ioctl+0x95 -> tty_ioctl+0x515
tty_ioctl+0x537 -> tty_ldisc_deref+0x0
tty_ldisc_deref+0x11 -> ldsem_up_read+0x0
ldsem_up_read+0x1b -> tty_ldisc_deref+0x16
tty_ldisc_deref+0x19 -> tty_ioctl+0x53c
tty_ioctl+0x543 -> tty_ioctl+0x1e2
tty_ioctl+0x20e -> __x64_sys_ioctl+0xa4
__x64_sys_ioctl+0xac -> __x64_sys_ioctl+0x49
__x64_sys_ioctl+0x50 -> __x64_sys_ioctl+0x5a
__x64_sys_ioctl+0x6f -> bpf_trampoline_6442593479+0x31
完美的过滤,完美的输出,不任何一点运气,不需要一点 tty 的内核实现前置知识,只需要找到 syscall 入口函数 __x64_sys_ioctl,然后 bpfsnoop will guide you home.
我仔细 review 哪一步是运气,意识到 retsnoop 这一步就是狗屎运:
# sudo ./retsnoop -x EIO -a 'tty' -e 'tty'
10:10:44.178847 -> 10:10:44.178850 TID/PID 30657/30657 (python/python):
entry_SYSCALL_64_after_hwframe+0x76
do_syscall_64+0x7e
x64_sys_call+0x131e
__x64_sys_ioctl+0xa4
2us [-EIO] tty_ioctl+0x515
⌇ 1us [-EIO] n_tty_ioctl+0x85
⌇ 1us [-EIO] n_tty_ioctl_helper+0x2d
⌇ 0us [-EIO] tty_mode_ioctl+0x1c1
set_termios+0x5d
⌇ 0us [-EIO] tty_check_change+0x13
!⌇ 0us [-EIO] __tty_check_change
halfling 的好运,返回 EIO 的内核函数名正好在 tty pattern 里,所以直接过滤出来了。但凡运气平凡一点,恐怕还要在摸索个半小时的内核才能找到返回 EIO 的地方。
这其实是个一般化的难题:已知用户态进程调用 syscall 报错,问内核为什么报错了。
retsnoop 已经算是很好的工具了,但还不够好,我仔细想了一会儿,意识到在 tracepoint 出口做 lbr trace 就是我想要的,而这 bpfsnoop 已经实现了。
预备,开始
$ sudo bpfsnoop -k __x64_sys_ioctl --output-lbr --filter-pid $(pidof python) --filter-arg 'retval==-5'
LBR stack:
[#31] is_current_pgrp_orphaned+0x4c -> __tty_check_change+0xd2
__tty_check_change+0xd6 -> __rcu_read_unlock+0x0
__rcu_read_unlock+0x24 -> __tty_check_change+0xdb
__tty_check_change+0xe0 -> __tty_check_change+0x30
__tty_check_change+0x46 -> tty_check_change+0x13
tty_check_change+0x18 -> set_termios+0x5d
set_termios+0x90 -> tty_mode_ioctl+0x1c1
tty_mode_ioctl+0x1c1 -> tty_mode_ioctl+0xc4
tty_mode_ioctl+0xf0 -> n_tty_ioctl_helper+0x2d
n_tty_ioctl_helper+0x3d -> n_tty_ioctl+0x85
n_tty_ioctl+0x95 -> tty_ioctl+0x515
tty_ioctl+0x537 -> tty_ldisc_deref+0x0
tty_ldisc_deref+0x11 -> ldsem_up_read+0x0
ldsem_up_read+0x1b -> tty_ldisc_deref+0x16
tty_ldisc_deref+0x19 -> tty_ioctl+0x53c
tty_ioctl+0x543 -> tty_ioctl+0x1e2
tty_ioctl+0x20e -> __x64_sys_ioctl+0xa4
__x64_sys_ioctl+0xac -> __x64_sys_ioctl+0x49
__x64_sys_ioctl+0x50 -> __x64_sys_ioctl+0x5a
__x64_sys_ioctl+0x6f -> bpf_trampoline_6442593479+0x31
完美的过滤,完美的输出,不任何一点运气,不需要一点 tty 的内核实现前置知识,只需要找到 syscall 入口函数 __x64_sys_ioctl,然后 bpfsnoop will guide you home.