/proc, /sys & sysctl

The kernel as a filesystem you can read.

Advanced12 min · lesson 10 of 17

Most files on your computer are lumps of data sitting on a disk: a photo, a log file, last quarter's spreadsheet. The files under /proc and /sys are a different animal. Nothing is stored there. Each time you read one, the kernel (the core part of the operating system that talks directly to the hardware) builds the answer on the spot out of whatever is true at that instant. A factory control room puts every reading on one wall. Some gauges are read-only dials showing pressure, temperature, and load. Others are knobs an operator can turn to change how the line runs. /proc and /sys are that wall for your kernel, written out as plain text you read with cat and turn with echo.

This has a name you have probably heard: on Linux, almost everything is a file. It sounds like a slogan until you notice what it buys you. To ask how much memory a process is using, or whether a security mitigation is switched on, you do not reach for a special programming interface. You read a file. And the tools you lean on all day (ps, top, free, uptime) are themselves reading these same files and dressing up the result, so going to the source shows you what the kernel actually reports, before any tool rounds it off, summarizes it, or in the worst case gets tricked into lying to you.

A Filesystem That Lives Nowhere

Start by proving the 'nothing on disk' claim to yourself. Ask df, the disk-usage tool, how big these filesystems are.

~/secopslog — bash
$ df -hT /proc /sys
Filesystem Type Size Used Avail Use% Mounted on proc proc 0 0 0 - /proc sysfs sysfs 0 0 0 - /sys

Zero size, zero used, zero available. /proc and /sys are pseudo-filesystems (also called virtual filesystems): directory trees the kernel mounts at boot and fills in on demand, with no blocks on any drive behind them. Every open() and read() is really a function call into the kernel that returns freshly computed text. You can even see the guard rails on the mount.

~/secopslog — bash
$ mount | grep -w /proc
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)

Those options are worth a glance. nosuid, nodev, and noexec mean you cannot run programs or honor special device files out of /proc, a sensible default that shuts off a few tricks. Now the read-only dials. Three files answer the first questions you ask about any box: what processor it runs on, how much memory it has, how hard it is working.

~/secopslog — bash
$ grep -m1 'model name' /proc/cpuinfo head -n3 /proc/meminfo cat /proc/loadavg
model name : Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz MemTotal: 8129404 kB MemFree: 243280 kB MemAvailable: 5901664 kB 0.42 0.51 0.55 2/834 28194

Read that last line, /proc/loadavg, left to right: the system's load averaged over 1, 5, and 15 minutes, then 2/834, which means 2 tasks are runnable right now out of 834 that exist, and finally 28194, the process ID the kernel handed out most recently. free and uptime are formatters over meminfo and loadavg, nothing more.

Per-Process Ground Truth

Every running program gets its own numbered folder under /proc, named by its PID (process identifier, the number the kernel uses to track it). Think of it as a live case file the kernel keeps open for each process and rewrites continuously. ps and top skim a few pages of that file and print a neat table. When you need the truth instead of a summary, you open the folder yourself.

~/secopslog — bash
$ ls /proc/1204/
cgroup cmdline comm cwd environ exe fd fdinfo limits maps mem mounts net ns root smaps stat statm status syscall task wchan

A quick tour of the entries that matter. cmdline is the exact command the process was started with. environ is its environment variables. exe links to the real binary (the compiled program file) on disk. cwd links to its working directory. fd/ lists every open file and socket (a live network connection) it holds. maps is its memory layout. limits shows its resource ceilings. status is a readable summary of all of it. Four of these pay for themselves during an incident.

Here is the one that bites teams in production. cmdline is world-readable. Any local user can read the full argument list (argv, the command-line arguments a program was started with) of any process on the box, including root's.

~/secopslog — bash
$ id -un tr '\0' '\n' < /proc/1337/cmdline
webapp python3 manage.py runserver --db-password=hunter2

That process, PID 1337, belongs to another team. The webapp user read its database password out of thin air, no privileges needed, because someone passed the secret as a command-line flag. The kernel stores arguments separated by null bytes, which is why we translate them to newlines with tr. The operator takeaway: pass secrets through environment variables or files, never as arguments, and treat argv as public within the host.

The environment is stricter. environ is readable only by the process's owner and by root, so an unrelated user hits a wall.

~/secopslog — bash
$ cat /proc/1337/environ sudo tr '\0' '\n' < /proc/1337/environ | grep -i secret
cat: /proc/1337/environ: Permission denied AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

Better, but not a safe. Root can read it, the process can leak it, and anything that can dump the process memory can pull it out. Environment variables beat command-line flags for secrets; they are not a vault. If you want a vault, mount the secret as a file with tight permissions.

One entry deserves a look on any host you suspect. exe is a symbolic link (a small file that points at another file) to the actual binary the process is executing.

~/secopslog — bash
$ sudo ls -l /proc/3921/exe
lrwxrwxrwx 1 root root 0 Jul 17 09:41 /proc/3921/exe -> '/dev/shm/.x/kdevtmpfsi (deleted)'

A healthy daemon (a long-running background service) points at something like /usr/sbin/nginx. This one points into /dev/shm (a memory-backed scratch directory) at a hidden folder, and the kernel has tagged it (deleted). The binary unlinked itself from disk right after launching and now runs only from memory. That is a textbook pattern for a crypto-miner or fileless malware: ps shows a believable name, while /proc/<pid>/exe shows where the code really came from.

The everyday reads matter too. How many descriptors is it holding, how many is it allowed, how much memory is resident, how many threads.

~/secopslog — bash
$ sudo ls /proc/1204/fd | wc -l grep 'open files' /proc/1204/limits grep -E 'VmRSS|Threads' /proc/1204/status
17 Max open files 1024 4096 files VmRSS: 45210 kB Threads: 4

A file descriptor is the numbered handle the kernel gives a process for each open file or socket. This process holds 17 and is allowed 1024 (its soft limit) up to 4096 (its hard ceiling). VmRSS is resident set size (RSS), the memory this process actually has in RAM (the physical working memory) right now, as opposed to memory it has reserved but not touched. Threads is the count of threads it is running. When a service is leaking descriptors, this is where you watch the number climb toward its limit, before it starts failing with 'Too many open files'.

Reading /proc directly is also how you catch a process hiding from ps. Because ps builds its list from /proc, a rootkit that only doctors ps output, or a process using odd tricks to stay off the list, can be exposed by walking /proc yourself and comparing. A PID with a live folder that never appears in ps is a lead worth pulling.

sysctl and /proc/sys: The Kernel's Control Panel

Everything under /proc/sys is a knob rather than a dial. These are the kernel's tunable settings, and unlike a CPU model or a load average, you are allowed to write to them. sysctl is the labeled control panel bolted over those same files. It exists because dotted names read more easily than long paths, and because of one trick worth burning into memory: a sysctl name is the file path under /proc/sys with the slashes turned into dots.

~/secopslog — bash
$ sysctl vm.swappiness cat /proc/sys/vm/swappiness
vm.swappiness = 60 60

Same value, two doors. sysctl vm.swappiness and cat /proc/sys/vm/swappiness open the identical file. vm.swappiness sets how eager the kernel is to move memory out to swap (disk space the kernel uses as an overflow area when RAM fills up), on a scale from 0 to 100, where 60 is the stock setting. On a database host you often want it lower, so the kernel keeps hot data in RAM instead of paging it out to disk the moment memory gets tight.

~/secopslog — bash
$ sudo sysctl -w vm.swappiness=10 cat /proc/sys/vm/swappiness
vm.swappiness = 10 10

That write is live and instant. It is also temporary. Reboot and you are back to 60, because sysctl -w only pokes the running kernel and forgets. To make a value survive a reboot, put it in a file the system reads on every boot.

/etc/sysctl.d/99-tuning.conf
# Persisted kernel tuning. Applied at boot and by `sysctl --system`.
vm.swappiness = 10
net.core.somaxconn = 1024
~/secopslog — bash
$ sudo sysctl --system
* Applying /usr/lib/sysctl.d/50-default.conf ... kernel.core_uses_pid = 1 * Applying /etc/sysctl.d/99-tuning.conf ... vm.swappiness = 10 net.core.somaxconn = 1024 * Applying /etc/sysctl.conf ...
There is no save button
A write into /proc/sys or /sys hits the running kernel the instant you press enter, with no confirmation and no undo. A wrong network buffer size or an aggressive memory setting can stall or crash a busy host. Change one value, watch the effect, and only then write it to /etc/sysctl.d. Do not paste a list of 'performance sysctls' from a forum wholesale; the box it came from was not yours.
Persisted is not the same as applied the way you think
Files in /etc/sysctl.d/ load in filename sort order, so 99-tuning.conf wins over 10-network.conf wherever they set the same key. There is a second trap. A running service can rewrite a value after boot, right over whatever your file said. Docker is the classic example: it flips net.ipv4.ip_forward to 1 the moment it starts. If a setting keeps reverting, run sysctl --system, read which file applied last, and check whether some daemon is changing it behind your back. And remember that sysctl -w never persists on its own.

For a defender, /proc/sys is also where a large slice of host hardening lives, and where you confirm it stayed hardened. A few settings are worth reading on every box.

~/secopslog — bash
$ sysctl kernel.randomize_va_space kernel.dmesg_restrict \ kernel.kptr_restrict net.ipv4.conf.all.rp_filter
kernel.randomize_va_space = 2 kernel.dmesg_restrict = 1 kernel.kptr_restrict = 1 net.ipv4.conf.all.rp_filter = 2

Read plainly: randomize_va_space = 2 means full ASLR (Address Space Layout Randomization, the kernel shuffling where code and data land in memory so an attacker cannot predict addresses), where 0 would mean it is off, a red flag. dmesg_restrict = 1 stops non-root users from reading the kernel log, which often leaks useful addresses. kptr_restrict = 1 hides kernel pointer values (internal memory addresses) from /proc for the same reason. rp_filter = 2 turns on reverse-path filtering in loose mode: the kernel drops a packet when its source address has no route back out through any interface, a cheap way to bin obviously spoofed (forged-source) traffic. Setting it to 1 is strict mode, which insists the return route match the exact interface the packet came in on.

Attackers write sysctls too. A common move after landing on a host is to turn it into a router so traffic can pivot through it, by setting net.ipv4.ip_forward to 1. Watching that one value is cheap. When you set it back, confirm the file itself, not only the command's echo, because the file is the ground truth.

~/secopslog — bash
$ sudo sysctl -w net.ipv4.ip_forward=0 cat /proc/sys/net/ipv4/ip_forward
net.ipv4.ip_forward = 0 0

/sys: Where the Hardware Shows Up

If /proc is mostly about processes and the live kernel, /sys is the building directory for hardware. sysfs (the filesystem mounted at /sys) is a tree the kernel builds to expose devices, drivers, and their settings as files. Your desktop, the udev device manager, and monitoring agents all read it constantly.

~/secopslog — bash
$ cat /sys/class/net/eth0/address cat /sys/block/sda/queue/rotational
02:42:ac:11:00:02 0

The first file is the MAC (Media Access Control) address, the hardware identifier tied to a network interface card. The second answers whether a disk spins: rotational = 0 means a solid-state drive, 1 means a spinning platter. On cloud hosts your names will differ (ens5 for the interface, nvme0n1 for the disk), but the files sit in the same place. This is how tooling learns your hardware without asking you.

The /sys entry security teams should know by name is the kernel's own report card on CPU hardware flaws.

~/secopslog — bash
$ grep -r . /sys/devices/system/cpu/vulnerabilities/ 2>/dev/null
/sys/devices/system/cpu/vulnerabilities/meltdown:Not affected /sys/devices/system/cpu/vulnerabilities/spectre_v1:Mitigation: usercopy/swapgs barriers and __user pointer sanitization /sys/devices/system/cpu/vulnerabilities/spectre_v2:Mitigation: Enhanced IBRS, IBPB: conditional, RSB filling /sys/devices/system/cpu/vulnerabilities/mds:Not affected /sys/devices/system/cpu/vulnerabilities/retbleed:Not affected

Each file is the kernel's verdict on one CPU flaw: whether this machine is exposed and what mitigation is active. You will see Not affected, Mitigation: followed by the technique in use, or the single word Vulnerable, which is the one to hunt for across a fleet. It is one grep per host, no agent and no vendor scanner required.

Closing the Doors You Left Open

Remember that cmdline is world-readable, which lets any local user inventory every process and its arguments. On a shared or multi-tenant host you can narrow that view. Mounting /proc with the hidepid option hides other users' process folders from them.

/etc/fstab
# Hide other users' processes from unprivileged accounts.
proc /proc proc defaults,hidepid=2 0 0
~/secopslog — bash
$ sudo mount -o remount,hidepid=2 /proc # apply now grep hidepid /proc/mounts # confirm it stuck
proc /proc proc rw,nosuid,nodev,noexec,relatime,hidepid=invisible 0 0

hidepid=2 (shown as invisible in /proc/mounts on modern kernels) means an unprivileged user sees only their own processes and cannot even tell that anyone else's exist. That shuts the argv-harvesting door from earlier for normal users. It does not stop root, and it can trip up monitoring agents that expect to see the whole process table, so roll it out and test before you depend on it.

The Kernel as Three Trees of Files
/proc/<pid>: per-process case files
cmdline
argv, world-readable (secrets leak)
environ
env vars, owner + root only
exe
real binary, flags (deleted)
fd, maps, status
open files, memory, threads
/proc/sys via sysctl: knobs you turn
vm.swappiness
paging aggressiveness 0-100
kernel.randomize_va_space
ASLR: 2 on, 0 off
net.ipv4.ip_forward
routing, attackers flip to 1
/etc/sysctl.d/*.conf
persist across reboot
/sys (sysfs): hardware & kernel objects
class/net/*/address
network card MAC address
block/*/queue/rotational
0 = SSD, 1 = spinning disk
cpu/vulnerabilities/*
Spectre / MDS mitigation status
All virtual, nothing on disk. Every read is a live question to the kernel; every write lands on it instantly.
Quick check
01You run sudo sysctl -w vm.swappiness=10 on a database host, the box behaves better all afternoon, then a reboot puts the value back at 60. What explains that?
Incorrect — At boot the system reads /etc/sysctl.conf and the files in /etc/sysctl.d, so anything you put there does come back on its own.
Incorrect — That command replays the config files already on disk. It has no way to capture a value you only typed at a prompt.
Correct — Tuning the running kernel and tuning across reboots are two separate jobs, and the second one means writing the key into a file.
Incorrect — Everything below /proc/sys is writable, which is why the number moved the moment you set it and the host felt different.
02You are an ordinary user on a shared host, and PID 1337 belongs to another team under a different account. What can you read out of /proc/1337?
Correct — That gap is why a password handed over as a flag is public on the box, while one in the environment is at least limited to two parties.
Incorrect — Only the environment carries that restriction. Any account on the host can list the arguments of any process, root's included.
Incorrect — This has the pair backwards. The argument list is the exposed one and the environment block is the guarded one.
Incorrect — Files the kernel synthesizes still carry ownership and permissions, and the ones on environ stop an unrelated user cold.
03sudo ls -l /proc/3921/exe shows the link pointing at '/dev/shm/.x/kdevtmpfsi (deleted)', while ps lists the process under an unremarkable name. What is the most likely read?
Incorrect — The kernel will not delete a running program's file to free space. Whatever holds that path did the unlinking itself.
Incorrect — A live folder under /proc means a live process. The (deleted) marker says the file went away while the code kept executing.
Incorrect — A healthy daemon's exe resolves to an on-disk path such as /usr/sbin/nginx, with no deleted marker hanging off the end.
Correct — A vanished image plus a hidden directory under /dev/shm is the shape of a crypto-miner or fileless malware, and ps will show you none of it.

The habit that pays off: when a tool tells you something surprising, find the file behind it and read that. cat /proc/sys/net/ipv4/ip_forward, ls -l /proc/<pid>/exe, grep . /sys/devices/system/cpu/vulnerabilities/*. The kernel is not keeping secrets. It has already written the answer to a file, and that file is current as of the instant you ask.

Try this

Work through “Closing the Doors You Left Open” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: there is no save button. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related