Master the fundamental concepts of system calls & kernel interface through this focused micro-challenge.
Three hints are available for this task, revealed one at a time inside the code workspace so you can struggle productively before seeing them.
Every task includes starter code, theory, and hidden tests so you can implement and verify locally in the browser.
How it worksptrace lets one process observe and control another: stop on syscalls, inspect registers, rewrite memory. Debuggers and strace build on this interface, though modern Linux prefers ptrace alternatives for performance.
Tracer workflow:
PTRACE_ATTACH or trace-at-forkSIGTRAP at syscall entry/exit stopsorig_rax for syscall number and argumentsPTRACE_SYSCALL to continueFor example, intercepting write(fd=1, ...) lets you log payloads before the tracee resumes.
ptrace is the exact syscall gdb, strace, and Docker's --cap-add=SYS_PTRACE debugging workflows are built on, and it's also how sandboxes like early gVisor and rr's record-and-replay debugger intercept every syscall a traced process makes. The orig_rax-versus-rax distinction you'll implement here (entry vs exit stop) is a real, easy-to-get-backwards detail that trips up first-time strace-clone authors.
Before you call the implementation done, walk failure modes on purpose. Test empty structures, single-element edge cases, maximum concurrency, and errno paths that must not crash the program. OS code usually fails in production when happy-path tests pass but invariants break under contention or memory pressure.
Keep structures small and name fields after kernel counterparts when possible. That lets you read man pages and kernel source side by side while you work. Print observable events during development; remove noisy logs once tests pass reliably.
You will write a tracer that prints syscall name and arguments for each entry. Understanding stop points matters because off-by-one continue calls hang the tracee forever.
Write the core loop of an strace-like tracer built on ptrace(PTRACE_SYSCALL). Each time the tracee stops, the tracer reads its registers. Syscall stops come in pairs (entry, then exit), and ptrace does not say which one this is, so the tracer has to keep track itself. The tracer decodes arguments, reads strings out of tracee memory (PTRACE_PEEKDATA), prints one line per call, and can deny a syscall. To deny, it sets orig_rax = -1 at entry so the kernel skips the call, then writes -errno into rax at exit.
cLoading…
call(args) = result, using the rax from the exit stop.exit_group(N) = ? at its entry, and do not expect an exit stop. <unfinished ...>. The later exit prints <... NAME resumed> = result. Every signal stop prints --- SIG --- (re-injected with PTRACE_SYSCALL).= -1 ERRNO (denied by tracer). = ? first.exited/killed (+++ exited with N +++ / +++ killed by SIG +++), or at the end of input, then print N syscalls traced, M denied.| nr | Format |
|---|---|
| 0 | read(fd, 0xADDR, count) |
| 1 | write(fd, "text", count): at most count bytes and at most 32 characters, with ... after the quote if the text was cut. Unknown memory prints 0xADDR |
| 3 | close(fd) |
| 39 | getpid() |
| 231 | exit_group(code) |
| 257 | openat(dirfd, "path", flags): -100 is AT_FDCWD. Flags are O_RDONLY/O_WRONLY/O_RDWR (low 2 bits), plus |O_CREAT (0x40), |O_TRUNC (0x200) and |O_APPEND (0x400) |
| other | syscall_NR(a0, a1, a2) |
A string argument may point into the middle of a mem block. Results from -4095 to -1 print as -1 NAME for EPERM 1, ENOENT 2, EINTR 4, EBADF 9, EAGAIN 11, EACCES 13, EFAULT 14, EINVAL 22 and ENOSYS 38 (E? otherwise). Any other result prints in decimal. Quoted text escapes \n, " and \\.
Input:
cLoading…
Output:
cLoading…
Hidden tests cover long and truncated write buffers, pointers into the middle of a string, unknown memory, openat flag combinations, several deny rules, a signal between two syscalls (nothing unfinished), a signal in the middle of a syscall, a tracee killed in the middle of a syscall, unknown syscall numbers, and trace input that ends without an exit.