A Look Inside /proc/[pid]/mem
While working on stack unwinding code in gVisor, a colleague ran into a puzzling issue: debug symbols from crash logs inside the sandbox were unreadable. The logging library was trying to open /proc/self/mem to examine ELF headers at the start of memory-mapped regions, a technique that lets unwinders calculate offsets for debug symbols without dereferencing raw addresses in unsafe contexts like a SIGSEGV handler. But inside gVisor, open() on that file returned ENOENT—the virtual file system in the Sentry component simply hadn't implemented it.
Implementing /proc/[pid]/mem in gVisor meant replicating the Linux kernel's behavior, which turned out to be more involved than a typical file operation.
Opening the File: Permissions Are Everything
The mem file provides direct raw access to a process's virtual address space. The manpages document open(), read(), and lseek() operations, with typical uses being debugging and memory dumps. But the real complexity lies in the access checks performed at open time.
When a process tries to open the file, the kernel retrieves the target task and calls mm_access. This is where permission validation kicks in: if the current task and the target task don't share the same memory manager, the kernel invokes __ptrace_may_access to determine if the requester would be allowed to attach via ptrace with PTRACE_MODE_ATTACH_FSCREDS credentials. The check doesn't require an active PTRACE_ATTACH, only the permission to attach.
Access is granted if any of these conditions hold:
- The current task belongs to the same thread group as the target task.
- The current task has
CAP_SYS_PTRACEwithin the target process's user namespace. - The credentials of the current and target tasks match (using file-system or real UID/GID depending on the
FSCREDS/REALCREDSmode), the target task is marked dumpable, they share a user namespace, and the target's capabilities are a subset of the current task's capabilities.
Additionally, the commoncap Linux Security Module (the foundation for SELinux and AppArmor) applies its own checks based on effective or permitted capabilities, requiring either that the current task's capabilities are a superset of the target's, or that the current task holds CAP_SYS_PTRACE in the target's user namespace.
Reading and Writing Process Memory
Since all permission checks happen at open time, read() and write() calls are more direct. The kernel routes them through mem_rw, which copies data in a loop through an intermediate page to minimize memory usage. mem_rw also employs the FOLL_FORCE flag, bypassing normal page permission checks so it can access pages marked non-readable or non-writable on user-owned memory.
Error handling has some quirks worth noting:
- If the target task exits after the file descriptor is opened, a subsequent
read()returns 0 bytes rather than an error. - If the initial copy from target memory to the intermediate page fails, the kernel only returns an error if no data has been read so far.
The file also supports lseek(), with the notable exception of SEEK_END.
Bringing It to gVisor
gVisor's implementation could lean on existing infrastructure: the sandbox kernel already had ptrace_may_access logic implemented as kernel.task.CanTrace, avoiding a full reimplementation of ptrace access rules. The gVisor version is simpler in one respect—it lacks support for PTRACE_MODE_FSCREDS, which remains an open issue in the project.
The natural place for access checks is in the GetFile method, which runs when a file descriptor is opened. After a successful check, the method returns an fs.File that implements standard operations like Read() and Write(). gVisor also provides primitives to handle generic operations such as lseek() without writing them from scratch.
When a task issues a read, the file's Read method retrieves the memory manager of the target task and uses gVisor's CopyIn/CopyOut helpers, which mirror the io.Reader and io.Writer interfaces. This yields readable stack traces on crashes inside the sandbox, a significant improvement for debugging isolated workloads.
The /proc/[pid]/mem file is easy to describe yet governed by a dense web of permission rules that protect highly-sensitive process memory. Recreating it within a sandboxed environment requires faithfully porting that logic—work that pays off when you're staring at a stack trace instead of an empty log.
![Diving into /proc/[pid]/mem](/covers/8e61aa71dc.webp?v=8564853)


