Linux File System Hunting: Dispatches from Inside the Machine
Most people treat the Linux filesystem as a backdrop — a place to store files. But if you know where to look, it is a living dashboard of the entire operating system. This blog is the result of hunting through that landscape on a real Ubuntu 24.04 LTS system, looking not for commands to memorise, but for stories the system is quietly telling.
Finding 1 — /proc/self: The Mirror the Kernel Holds Up to Every Process
Every running process on Linux has its own private window into the kernel at /proc/<PID>/. Reading /proc/self is a shortcut — it always refers to the process doing the reading.
When I opened /proc/self/fd, I found this:
lrwxrwxrwx 0 -> pipe:[15] (stdin)
lrwxrwxrwx 1 -> pipe:[16] (stdout)
lrwxrwxrwx 2 -> pipe:[17] (stderr)
These are not placeholders. Each is a live symlink to the actual kernel object behind stdin, stdout, and stderr. The numbers 15, 16, 17 are inode identifiers in the kernel's pipefs. This is how shell piping actually works — the shell sets up pipe inodes and wires your process's FD 0/1/2 to them before execve() is called.
/proc/self/maps was even more revealing — it shows the full virtual memory layout:
55bbb50c5000-55bbb50c7000 r--p /usr/bin/head (ELF header, read-only)
55bbb50c7000-55bbb50cd000 r-xp /usr/bin/head (text segment, executable)
55bbb50d1000-55bbb50f2000 rw-p [heap]
7ecb459b0000-7ecb461b0000 rw-p [stack]
7edab0800000-7edab0828000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
Why it matters: The kernel enforces W^X (Write XOR Execute) at this level. The text segment is r-xp but never rw-. Shared libraries are mapped once into the kernel's page cache and Copy-on-Write'd into each process — which is how 50 bash processes can share one libc.so.6 in RAM without 50 copies of it.
Security insight: A process that maps a region as
rw-and then re-maps it asr-xis the classic shellcode injection footprint. Forensic tools detect this at the/proclevel.
Finding 2 — The DNS Stack Is Three Files Deep
Name resolution in Linux is not a single mechanism. It is an ordered chain controlled by three cooperating files:
/etc/nsswitch.conf — The Dispatch Table
hosts: files dns
The word files means check /etc/hosts first. The word dns means fall through to a resolver if /etc/hosts has no match. This one line is why you can block any domain by pointing it to 127.0.0.1 in /etc/hosts — without touching DNS servers at all.
/etc/hosts — The Local Authority
On this system:
# BEGIN CONTAINER MANAGED HOSTS
127.0.0.1 localhost
127.0.0.1 runsc
# END CONTAINER MANAGED HOSTS
The BEGIN/END CONTAINER MANAGED HOSTS comment markers reveal something important: this file was not user-managed. The container runtime injected these entries on startup. The hostname runsc is the kernel's identity inside the sandbox.
/etc/resolv.conf — The Upstream Resolver
nameserver 8.8.8.8
Why it matters: The full chain is: nsswitch.conf decides the order → /etc/hosts is checked first → /etc/resolv.conf points upstream if hosts fails. On systemd systems, /etc/resolv.conf is often a symlink to /run/systemd/resolve/stub-resolv.conf, routing all queries through a local stub resolver at 127.0.0.53 that handles caching and DNSSEC — invisible from resolv.conf alone.
Finding 3 — The Routing Table Hidden in ASCII
Network routing is usually checked with ip route. But the actual data lives in /proc/net/route as raw hex:
Iface Destination Gateway Flags Mask
3bcbb706c0-v 4A010415 00000000 0001 7FFFFFFF
3bcbb706c0-v 00000000 4B010415 0003 00000000
Decoding the little-endian hex with Python:
import socket, struct
def decode(h):
return socket.inet_ntoa(struct.pack('<I', int(h, 16)))
decode('4A010415') # → 21.4.1.74 (network)
decode('4B010415') # → 21.4.1.75 (default gateway)
The Flags column tells the full story: 0001 = route is Up. 0003 = Up + Gateway. The second entry with destination 00000000 and Mask 00000000 is the default route — it catches all unmatched traffic and hands it to the gateway.
The interface name 3bcbb706c0-v (not a typical eth0 or ens3) immediately signals something: the -v suffix is gVisor's virtual network interface naming convention.
Why it matters: Reading /proc/net/route directly is how network monitoring tools like netstat and ss actually work. They don't shell out to ip route — they parse this file. It's faster and requires no external binary.
Finding 4 — /proc/net/tcp: Active Sockets as a Hex Table
Every TCP socket on the system is listed in /proc/net/tcp:
sl local_address rem_address st
3: 00000000:07E8 00000000:0000 0A ← LISTEN
41: 4A010415:07E8 963C040A:DF70 01 ← ESTABLISHED
Port
0x07E8→ decimal 2024State
0A→ LISTENState
01→ ESTABLISHEDRemote IP
963C040A→10.4.60.150(decoded little-endian)Remote port
0xDF70→ 57200 (ephemeral port)
The ESTABLISHED entry is the very TCP session this process is running inside.
Security implication:
/proc/net/tcpis world-readable on most Linux systems. Any local user can enumerate every listening port and active connection, including the UID that owns each socket. Attackers with local access routinely parse this file to map the network attack surface before escalating privileges.
Finding 5 — /proc/self/cgroup: You Are Where the Container Put You
cgroups (control groups) enforce resource limits on groups of processes. Every process belongs to a cgroup hierarchy, visible at /proc/self/cgroup:
7:pids:/container_012bqMcAkkVq7TwVzQUrxe5g--wiggle--9bc569
6:memory:/container_012bqMcAkkVq7TwVzQUrxe5g--wiggle--9bc569/process_api/41a14caf...
5:job:/container_012bqMcAkkVq7TwVzQUrxe5g--wiggle--9bc569
1:cpu:/container_012bqMcAkkVq7TwVzQUrxe5g--wiggle--9bc569
Each line: hierarchy_id : subsystem : cgroup_path. The process is in separate cgroups for PIDs, memory, CPU, cpuacct, and cpuset — each managed independently. The memory subsystem has a nested path, meaning this process is in a sub-cgroup within the container's cgroup — allowing per-process memory limits inside the container boundary.
The memory limit at /sys/fs/cgroup/memory/memory.limit_in_bytes was 9223372036854771807 — the maximum int64 value, meaning "no hard limit" at this cgroup level. Limits are enforced at the container runtime layer above.
Why it matters: Docker, Kubernetes, and all container runtimes are fundamentally cgroup managers. The container ID in the cgroup path is the container. docker stats works by reading /sys/fs/cgroup/memory/<container_id>/memory.usage_in_bytes — not some internal Docker API.
Finding 6 — /proc/self/ns: Linux Namespaces Are Just Inodes
Linux containers are built on namespaces — kernel mechanisms that give a process an isolated view of the system. Every namespace a process belongs to is visible in /proc/self/ns/:
lrwxrwxrwx ipc -> ipc:[2]
lrwxrwxrwx mnt -> mnt:[5]
lrwxrwxrwx net -> net:[1]
lrwxrwxrwx pid -> pid:[4]
lrwxrwxrwx user -> user:[1323]
lrwxrwxrwx uts -> uts:[3]
Each namespace is a symlink pointing to a kernel object identified by an inode number. Two processes sharing the same inode number share the same namespace — they see the same network stack, filesystem mounts, and process tree. Different inodes = isolated.
The net namespace has inode 1 — a suspiciously low number suggesting proximity to the host's network namespace. This kind of detail matters in container escape analysis.
The insight: Namespaces have no names — only inodes. "Entering a container" means having your process re-associated with that container's set of namespace inodes. This is exactly what
docker execdoes: it callssetns()with file descriptors pointing to the target container's/proc/<pid>/ns/*files.
Finding 7 — The passwd/shadow Split: A Security Decision Made Visible
/etc/passwd is world-readable. On this system:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
The x in the password field is not the password — it is a pointer. It means: look in /etc/shadow. /etc/shadow holds the actual hashed password and is readable only by root and the shadow group:
-rw-r----- 1 root shadow 609 Apr 11 /etc/shadow
This split exists because programs like ls and id need to read /etc/passwd to display usernames, but must never read password hashes. Before shadow passwords (early Unix), /etc/passwd was readable by everyone — and so were the hashes.
Notice system accounts like daemon and bin have /usr/sbin/nologin as their shell. This is not just documentation — PAM checks the shell field. If it points to nologin, login is rejected before any password prompt appears.
/etc/login.defs insight:
UID_MIN 1000
UID_MAX 60000
Human users start at UID 1000. UIDs 1–999 are reserved for system accounts. A process running as UID 33 (www-data) cannot write files owned by UID 1000. This boundary is the first line of privilege isolation.
Finding 8 — setuid Binaries: Privilege Pinned to the Filesystem
Nine binaries on this system carry the setuid-root bit:
/usr/bin/chfn /usr/bin/chsh /usr/bin/fusermount3
/usr/bin/gpasswd /usr/bin/mount /usr/bin/newgrp
/usr/bin/passwd /usr/bin/su /usr/bin/umount
When you run /usr/bin/passwd as an ordinary user, it temporarily runs as root (UID 0) — long enough to write your new password hash to /etc/shadow, which is root-owned. The setuid bit makes this possible without giving you a root shell.
mount and umount being setuid-root is equally interesting. A user can mount a filesystem, but only within the constraints the kernel imposes on non-root mount calls. The setuid bit runs the syscall as root, but the kernel still validates mount options and filesystem type.
Security implication: Every setuid binary is a potential privilege escalation vector if it has an exploitable bug. Security hardening often involves removing setuid bits that aren't needed (e.g.
chfn/chshon servers), replacing them with Linux capabilities, or confining them with AppArmor profiles.
Finding 9 — /etc/skel: The Blueprint for Every New User
When you create a user with useradd, Linux doesn't just write a line to /etc/passwd. It copies the entire contents of /etc/skel into the new user's home directory:
/etc/skel/.bash_logout ← runs when the user logs out
/etc/skel/.bashrc ← interactive shell configuration
/etc/skel/.profile ← login shell configuration
These three files are the skeleton of every new user's environment. .bashrc sets up aliases and shell options for interactive sessions. .profile sets PATH and invokes .bashrc for login shells. .bash_logout can wipe terminal history or run cleanup on exit.
An administrator who adds a company-wide alias or security banner places it in /etc/skel/.bashrc before creating accounts — every new user inherits it. Existing users are not affected.
Persistence insight: An attacker with write access to
/etc/skelcan plant malicious.bashrccode that executes as any new user who logs in. This is a real post-exploitation persistence technique — add a reverse shell callback to/etc/skeland wait for an admin to provision a new account.
Finding 10 — Container Runtime Fingerprinting Through the Filesystem Alone
By the end of the investigation, the filesystem had revealed something never explicitly documented anywhere: this is not a bare-metal Linux system. It's a gVisor-sandboxed container. The evidence:
| Artifact | What It Reveals |
|---|---|
Linux version 4.4.0 #1 SMP Sun Jan 10 15:06:54 PST 2016 |
Not a real 2016 kernel — gVisor's synthetic kernel version string |
Interface 3bcbb706c0-v in /proc/net/route |
gVisor's virtual network interface naming convention |
Mounts using 9p protocol in /proc/mounts |
gVisor uses Plan 9 filesystem protocol to expose host directories |
Hostname runsc in /proc/sys/kernel/hostname |
runsc is gVisor's sandbox runner binary |
Container ID in /proc/self/cgroup |
Full Anthropic process orchestration path visible |
None of this required any special tools. No docker inspect, no cloud metadata APIs, no external queries. Just reading files.
Closing Thoughts
Linux exposes almost everything through the filesystem. Processes publish their memory maps, open files, cgroup memberships, and namespace associations. The network stack publishes routing tables and TCP socket state in ASCII. The kernel's own tunable variables live in /proc/sys and /sys, readable without any special tools.
This design — everything is a file — is both Linux's greatest transparency feature and its most interesting security frontier.
The system is always talking. You just have to know which files to read.
Explored on Ubuntu 24.04 LTS (Noble Numbat) — April 2026
Tags: linux devops security systems-programming networking
