You could always run the application under:
gdb --args /path/to/your/your-program and its args
Then add breakpoints on unlink()
, unlinkat()
, rmdir()
functions or syscalls:
catch syscall unlink
catch syscall unlinkat
catch syscall rmdir
run
Then each time a breakpoint is reached, check that it's about deleting files in that directory and inspect the files in there or copy them elsewhere. Enter cont
in gdb to resume execution (until the next breakpoint).
Example with rm -rf
:
$ gdb -q --args rm -rf /tmp/tmp.HudBncQ4Ni
Reading symbols from rm...
Reading symbols from /usr/lib/debug/.build-id/f6/7ac1d7304650a51950992d074f98ec88fe2f49.debug...
(gdb) catch syscall unlink
Catchpoint 1 (syscall 'unlink' [87])
(gdb) catch syscall unlinkat
Catchpoint 2 (syscall 'unlinkat' [263])
(gdb) catch syscall rmdir
Catchpoint 3 (syscall 'rmdir' [84])
(gdb) run
Starting program: /bin/rm -rf /tmp/tmp.HudBncQ4Ni
Catchpoint 2 (call to syscall unlinkat), 0x00007ffff7eb6fa7 in __GI_unlinkat () at ../sysdeps/unix/syscall-template.S:120
120 ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) info registers
rax 0xffffffffffffffda -38
rbx 0x555555569830 93824992319536
rcx 0x7ffff7eb6fa7 140737352789927
rdx 0x0 0
rsi 0x555555569938 93824992319800
rdi 0x4 4
rbp 0x555555568440 0x555555568440
rsp 0x7fffffffda48 0x7fffffffda48
r8 0x3 3
r9 0x0 0
r10 0xfffffffffffffa9c -1380
r11 0x206 518
r12 0x0 0
r13 0x7fffffffdc30 140737488346160
r14 0x0 0
r15 0x555555569830 93824992319536
rip 0x7ffff7eb6fa7 0x7ffff7eb6fa7 <__GI_unlinkat+7>
eflags 0x206 [ PF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb) x/s $rsi
0x555555569938: "test"
(gdb) info proc
process 7524
cmdline = '/bin/rm -rf /tmp/tmp.HudBncQ4Ni'
cwd = '/export/home/stephane'
exe = '/bin/rm'
(gdb) !readlink /proc/7524/fd/4
/tmp/tmp.HudBncQ4Ni
(gdb) !find /tmp/tmp.HudBncQ4Ni -ls
1875981 4 drwx------ 2 stephane stephane 4096 Aug 8 09:30 /tmp/tmp.HudBncQ4Ni
1835128 4 -rw-r--r-- 1 stephane stephane 5 Aug 8 09:30 /tmp/tmp.HudBncQ4Ni/test
Here, the breakpoint was on the unlinkat()
system call for the test
entry inside /tmp/tmp.HudBncQ4Ni
on a x86_64 Linux system where the first two arguments of the syscall are in the rdi
and rsi
registers.
strace
can inject signals to a process when a syscall is called (strace -e inject=unlink,unlinkat,rmdir:signal=STOP
to suspend for instance), but AFAICT it always does it after the syscall returns, so once the file has already been removed.
You can however delay the entry so you can suspend by hand with Ctrl+Z for instance:
$ strace -e inject=unlink,unlinkat,rmdir:delay_enter=5s -e unlink,unlinkat,rmdir rm -rf /tmp/tmp.HudBncQ4Ni
unlinkat(4, "test", 0^Z
zsh: suspended strace -e inject=unlink,unlinkat,rmdir:delay_enter=10s -e rm -rf
Or, as suggested by @PhilippWendler, you can use:
strace -e inject=unlink,unlinkat,rmdir:retval=0 -e unlink,unlinkat,rmdir ...
or:
strace -e inject=unlink,unlinkat,rmdir:error=EACCES -e unlink,unlinkat,rmdir ...
To hijack the syscalls and pretend they succeed (with retval=0
) or fail (with EACCES
here meaning Permission denied) without actually calling them.
Both gdb
and strace
can attach to an already running process with --pid <the-process-id>
/ -p <the-process-id>
respectively. They can also be told to follow forks and execs and trace the children as well so you can attach to the parent and watch for or hijack unlinks in the children (see -f
in strace
and the follow-*
settings in gdb
)
/tmp
on your server? I'm not sure if there is a good native solution for Oracle Linux 9 but any number of user-space file systems could work, like one that mounts s3 buckets.