Stateful eBPF
Cross-syscall state tracking
In the previous exercise, we captured all read() calls. But what if we only care about reads from a specific file, like /tmp/password?
The problem: read() operates on file descriptors, not filenames. When a process calls open("/tmp/password"), the kernel returns a file descriptor (fd) - a per-process unique number (like 4). Subsequent read calls use that number: read(4, buf, ...).
By the time we see the read() syscall, we only have access to the fd. The filename is gone.
To filter reads by filename, we need to correlate the open() and read() syscalls: track which fds correspond to /tmp/password at open time, then use that information when intercepting reads.
Correlating open and read
We need to hook four events and pass information between them:
When a process opens /tmp/password:
- open entry - We see the pathname and can check if it matches our target
- open exit - We get the fd that was assigned to this file
When that same process reads from the fd:
- read entry - We see which fd is being read from and the destination buffer
- read exit - We see how many bytes were written
The challenge is connecting these dots: how do we know in step 3 that fd 4 is the one we care about from step 1?
Tracking with three maps
We’ll use three maps to pass information through this pipeline.
Temporary marker during open:
At open entry, we mark PIDs that are opening /tmp/password in open_curr_fd_interesting. At exit, we check this mark to know if the returned fd is interesting.
We can safely use PID as the key because the thread is blocked during the syscall - it can’t make another open() call until this one completes.
Persistent tracking of interesting fds:
Once we know that PID 123 got fd 4 for /tmp/password, we need to remember this association. Later, when we see read(4, ...), we can check if (123, 4) is in our map.
This requires our first composite key:
struct pid_fd_key {
u64 pid;
u32 fd;
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, struct pid_fd_key);
__type(value, u8); // marker
} open_interesting_fds SEC(".maps");
We need both PID and FD in the key because a process can have multiple file descriptors open simultaneously. Fd 4 might be /tmp/password while fd 5 is /etc/config.
Temporary storage during read:
Just like in the previous exercise, we’ll define read_curr_fd_buf to save the buffer pointer from read entry to read exit, using PID as the key.
The complete flow:
The challenge
A program opens and reads multiple files. Only /tmp/password contains the password.
Your task
Implement the four eBPF programs to correlate the open and read syscalls. The starter code has the map definitions and TODOs marking where to add your logic.
Get current process and thread ID
Compare two strings for equality
- Args:
char* bufdynamic buffer to compareu32 buf_szlength of dynamic bufferconst char* buf2literal string to compare against
Insert or update map entry
- Args:
void* mappointer to mapconst void* keypointer to keyconst void* valuepointer to valueu64 flagsBPF_ANY (create or update), BPF_NOEXIST (create only), or BPF_EXIST (update only)
Get value from map by key
- Args:
void* mappointer to mapconst void* keypointer to key
Remove entry from map
- Args:
void* mappointer to mapconst void* keypointer to key to delete