Operating System Security
Modern OS security is a multi-layered defense against both software vulnerabilities and hardware-level attacks. From memory protection to mandatory access control, understanding these mechanisms is crucial for building secure systems.1. Memory Protection Fundamentals
1.1 Page-Level Protection (NX/DEP)
No-Execute (NX) / Data Execution Prevention (DEP) marks memory pages as non-executable.- A page can be writable OR executable, but never both
- Prevents attacker from modifying code or executing data
- The logic is straightforward: if you can write it, the attacker can inject code there, so it must not execute. If it executes, it must be immutable.
mprotect() to flip them to RX before execution. This W-then-X pattern is audited carefully in security-critical JITs like Firefox’s Wasm compiler.
Check NX status:
1.2 Address Space Layout Randomization (ASLR)
Problem: Without ASLR, addresses are predictable.1.3 Stack Canaries (Stack Smashing Protection)
Stack canary is a random value placed on the stack between local variables and the return address.- Terminator Canary
- Random Canary
- XOR Canary
2. Control Flow Integrity (CFI)
CFI ensures program control flow follows legitimate paths (no arbitrary jumps).2.1 Forward-Edge CFI (Indirect Calls)
Problem: Function pointers can be hijacked.2.2 Backward-Edge CFI (Return Address Protection)
Shadow Stack: Hardware-protected copy of return addresses.3. Privilege Separation & Capabilities
3.1 Traditional Unix DAC (Discretionary Access Control)
3.2 POSIX Capabilities
Divide root privileges into distinct units:- Set File Capabilities
- Capability-Aware Code
- View Process Capabilities
- Ambient Capabilities
3.3 Seccomp (Secure Computing Mode)
Seccomp-BPF: Restrict system calls a process can make using BPF filters.4. Mandatory Access Control (MAC)
4.1 SELinux (Security-Enhanced Linux)
SELinux adds mandatory access control on top of DAC.Enforcing
Permissive
Disabled
4.2 AppArmor
AppArmor is path-based MAC (vs SELinux’s label-based).5. Microarchitectural Attacks & Mitigations
5.1 Spectre & Meltdown
Speculative Execution: CPU predicts branch and executes ahead, then discards if wrong.5.2 Rowhammer
DRAM vulnerability: Rapidly accessing one row can flip bits in adjacent rows.ECC Memory
Target Row Refresh (TRR)
Software Mitigations
OS-Level
6. Sandboxing Techniques
6.1 Namespaces (Containers)
Linux namespaces isolate resources between processes.6.2 Chrome Multi-Process Sandbox
6.5 Production Caveats and Common Pitfalls
Linux security primitives are individually well-designed. The failure mode is composition: each primitive looks correct in isolation, then a subtle assumption interaction creates an escape route. Below are four traps that bite even experienced security engineers, paired with the patterns that close them.7. Interview Questions & Answers
Q1: How does NX/DEP prevent code execution on the stack?
Q1: How does NX/DEP prevent code execution on the stack?
- Bit 63: NX (No-Execute) bit
- When set: Page cannot be executed (will fault with #PF if IP points here)
- When clear: Page is executable
- Attacker overflows buffer on stack
- Injects shellcode
- Overwrites return address to point to shellcode
- Function returns, jumps to shellcode address
- CPU checks NX bit → Page is not executable
- #PF (Page Fault) → Kernel kills process
- Stack/Heap: Writable, NOT executable
- Code: Executable, NOT writable
- Prevents: Code injection attacks
Q2: Explain ASLR and how it prevents exploitation. What are its weaknesses?
Q2: Explain ASLR and how it prevents exploitation. What are its weaknesses?
- Stack base address
- Heap base address
- Libraries (libc, etc.)
- Executable base (if PIE - Position Independent Executable)
- vDSO, vvar
- Stack: 19 bits → 524,288 possible positions
- Heap: 13 bits → 8,192 possible positions
- Libraries: 28 bits → 268 million possible positions
- PIE executable: 28 bits → 268 million possible positions
-
Information Leak:
- Pointer disclosure → calculate base addresses → bypass ASLR
- Format string bugs, memory corruption leaks
-
Entropy Limitations:
- 13 bits (heap) = 8,192 attempts
- If process doesn’t crash (fork server), brute-forceable
-
32-bit Systems:
- Limited address space → low entropy
- 8 bits library randomization → 256 attempts
-
Non-PIE Executables:
- Main executable at fixed address
- Contains ROP gadgets at known addresses
-
Cache Timing Attacks:
- Side-channel attacks can determine addresses
- Use PIE (Position Independent Executable)
- Fix information leaks
- Crash on exploit attempts (don’t fork)
- Use Control Flow Integrity (CFI)
- Combine with other defenses (NX, stack canaries)
Q3: How do stack canaries detect buffer overflows? Can they be bypassed?
Q3: How do stack canaries detect buffer overflows? Can they be bypassed?
- Buffer overflow overwrites local variables
- Overflow continues, overwrites canary
- Function returns
- Kernel checks: stack_canary == __stack_chk_guard?
- Mismatch → Stack smashing detected! → abort()
- Combine with ASLR (randomize canary address)
- Use fortified functions (_strcpy_chk) to prevent overflows
- Re-randomize canary after fork
- Stack Clash protection (prevent jumping over canary)
Q4: What is the difference between capabilities and setuid? Why are capabilities better?
Q4: What is the difference between capabilities and setuid? Why are capabilities better?
- Principle of Least Privilege: Only grant necessary permissions
- Reduced Attack Surface: Exploit gets limited capabilities, not full root
- Better Auditability: Clear why each capability is needed
- Flexibility: Can grant to non-root users
- Inheritance: Can design capability-aware services
- systemd services with capabilities
- Docker containers (run as non-root with specific capabilities)
- Network daemons (CAP_NET_BIND_SERVICE instead of setuid)
Q5: How does KPTI mitigate Meltdown? What is the performance cost?
Q5: How does KPTI mitigate Meltdown? What is the performance cost?
-
CR3 Write (page table switch):
- ~150-300 CPU cycles per switch
- 2 switches per syscall (enter + exit)
-
TLB Flush:
- Translation Lookaside Buffer caches virtual→physical address translations
- Changing CR3 flushes TLB (must reload from memory)
- TLB misses add ~100 cycles per memory access
-
Frequency of Syscalls:
- I/O-heavy workloads: Many syscalls → high overhead
- CPU-bound workloads: Few syscalls → low overhead
-
PCID (Process Context ID):
- Tag TLB entries with PCID
- Avoid full TLB flush on CR3 switch
- Reduces overhead to 1-5%
-
Lazy TLB Switching:
- Kernel threads don’t switch page tables
- Reuse previous user’s kernel mapping
-
CPU Microcode Updates:
- Intel CPUs without Meltdown bug → no KPTI needed
- Check:
cat /sys/devices/system/cpu/vulnerabilities/meltdown - If says “Not affected” → KPTI not active
Q6: Explain how Spectre works and why retpolines are an effective mitigation.
Q6: Explain how Spectre works and why retpolines are an effective mitigation.
- Retpolines are slower than direct jumps (5-20% overhead)
- But necessary for security on vulnerable CPUs
- Modern CPUs have hardware mitigations (IBRS - Indirect Branch Restricted Speculation)
- Return instructions are different: RSB not poisonable
- Speculation contained: Loop prevents speculative execution reaching gadgets
- Works on all CPUs: Software mitigation (doesn’t need hardware support)
- Comprehensive: Protects all indirect branches
- Performance overhead (modern CPUs use IBRS instead)
- Doesn’t protect against Spectre v1 (bounds check bypass)
- Doesn’t protect against other speculative execution attacks (L1TF, MDS, etc.)
Q7: Compare SELinux vs AppArmor. When would you use each?
Q7: Compare SELinux vs AppArmor. When would you use each?
Detailed Comparison:1. Security Model:SELinux:
- Type Enforcement (TE): Subjects (processes) have types, objects (files) have types
- Multi-Level Security (MLS): Confidentiality levels (Top Secret, Secret, etc.)
- Multi-Category Security (MCS): Categories for compartmentalization
- Very fine-grained control
- Path-based access control
- Capabilities control
- Network access control (protocol/address)
- Simpler model, easier to understand
- Label lookups in xattrs (extended attributes)
- Hash table lookups for policy decisions
- Overhead: 3-7% typically
- Path resolution for every access
- Simpler policy checks
- Overhead: 1-3% typically
- Requires filesystem with xattr support
- Labels stored as extended attributes
ls -Zshows labels- Relabeling filesystem can be slow
- No special filesystem requirements
- Works on any filesystem (even FAT, NFS)
- No labels to manage
- Maximum security required (government, military)
- Need MLS/MCS (confidentiality levels)
- Want very fine-grained control
- Already familiar with it (RHEL/Fedora/CentOS)
- Need label-based security (labels follow files even if moved)
- Simplicity preferred over maximum granularity
- Easier policy management desired
- Filesystem doesn’t support xattrs (NFS, FAT)
- Developers/admins less experienced with MAC
- Debian/Ubuntu/SUSE environment
- Docker/Podman use SELinux contexts
- Each container gets unique MCS label
- Container
svirt_sandbox_file_t, hostcontainer_file_t - Strong isolation via labels
- Docker uses AppArmor profiles
- Default profile restricts mount, capabilities, etc.
- Custom profiles for specific containers
- Path-based restrictions easier to understand
- Labels stored with files (xattrs)
- Policy is separate from filesystem
- Moving files between systems: labels can be lost
- Need to relabel after restore from backup
- Policy references absolute paths
- Moving profile to different system: works if paths same
- But path changes require profile updates
Recommendation Matrix:
Q8: How does seccomp-BPF work and why is it critical for container security?
Q8: How does seccomp-BPF work and why is it critical for container security?
Architecture:
BPF Filter Structure:
Container Security Use Case:Problem: Containers share kernel with host. Malicious container can exploit kernel vulnerabilities.Seccomp Solution: Reduce attack surface by blocking dangerous syscalls.Docker Default Seccomp Profile (simplified):
- Kernel Exploit Mitigation:
- Privilege Escalation Prevention:
- Attack Surface Reduction:
Implementing Custom Seccomp:Example: Strict Sandbox:
Debugging Seccomp Violations:
Why BPF:
- Efficiency: JIT-compiled to native code (fast!)
- Safety: BPF verifier ensures filter cannot crash kernel
- Flexibility: Can inspect syscall arguments, not just number
- Performance: Evaluated in kernel space (no context switch)
- Could only allow read/write/exit/_exit
- No flexibility
- Can allow specific syscalls
- Can inspect arguments (e.g., allow open but only for /tmp/*)
- Can return different actions (ERRNO, TRAP, LOG, ALLOW)
Limitations:
- Cannot inspect pointers: BPF cannot dereference user-space pointers (no access to path strings, only FDs)
- Time-of-check-time-of-use (TOCTOU): Arguments checked before syscall, but can change
- Bypass via allowed syscalls: If
write()allowed, attacker might abuse it - Complexity: Writing correct BPF filters is hard
Summary:Seccomp-BPF is critical for containers because:
- ✅ Reduces kernel attack surface (blocks ~1/3 of syscalls)
- ✅ Prevents privilege escalation (blocks namespace manipulation)
- ✅ Mitigates kernel exploits (blocks vulnerable syscalls)
- ✅ Fast (BPF JIT compilation)
- ✅ Flexible (programmable filters)
- ✅ Secure (BPF verifier prevents filter bugs)
12. Threat Modeling for OS-backed Services
When designing secure services, think systematically about OS-level attack surfaces.The STRIDE Model Applied to OS
Defense-in-Depth Checklist
Summary
Key Takeaways:- Memory Protection: NX/DEP, ASLR, and stack canaries are foundational defenses against memory corruption attacks.
- Control Flow Integrity: Forward-edge CFI and shadow stacks (backward-edge CFI) prevent control-flow hijacking.
- Privilege Separation: Capabilities provide fine-grained privileges instead of all-or-nothing root access.
- Mandatory Access Control: SELinux (label-based) and AppArmor (path-based) enforce policies beyond DAC.
- Microarchitectural Attacks: Spectre and Meltdown exploit speculative execution. KPTI and retpolines mitigate but with performance cost.
- Sandboxing: Namespaces, seccomp, and combinations thereof create strong isolation for untrusted code.
- ASLR + NX + Stack Canaries + CFI (memory safety)
- Capabilities + Seccomp + Namespaces (privilege reduction)
- SELinux/AppArmor (mandatory access control)
- KPTI + Retpolines + CPU features (hardware attack mitigation)
Interview Deep-Dive
Explain how ASLR, stack canaries, and NX bits work together to defend against buffer overflow attacks. What is the attack sequence an adversary must defeat, and where does each mitigation intervene?
Explain how ASLR, stack canaries, and NX bits work together to defend against buffer overflow attacks. What is the attack sequence an adversary must defeat, and where does each mitigation intervene?
-
Step 1: Overwrite the return address — The attacker provides input that overflows a stack buffer and overwrites the saved return address (RIP) on the stack, redirecting execution to attacker-controlled code.
- Stack canaries intervene here. A random value (the “canary”) is placed between local variables and the saved return address at function entry. Before the function returns, the compiler-inserted code checks if the canary was modified. If it was (because the overflow overwrote it on the way to the return address), the program aborts immediately. The attacker must either guess the canary (2^64 possibilities on 64-bit) or find a way to overwrite the return address without touching the canary (possible with format string bugs or non-contiguous overwrites, but much harder).
-
Step 2: Redirect execution to shellcode — If the attacker bypasses the canary, they redirect execution to injected code (shellcode) in the buffer itself.
- NX (No-Execute) / DEP intervenes here. The stack (and heap, and data sections) are marked non-executable at the page table level. The CPU enforces this in hardware: executing an instruction from an NX page triggers a page fault. The attacker’s shellcode on the stack cannot execute. This forces the attacker to use Return-Oriented Programming (ROP) — chaining existing code snippets (“gadgets”) from the binary and libraries.
-
Step 3: Locate usable code gadgets — The attacker needs to find executable code at known addresses to build ROP chains.
- ASLR intervenes here. The kernel randomizes the base addresses of the stack, heap, shared libraries, and (with KASLR) the kernel itself at each process start. The attacker cannot hard-code addresses of gadgets because they change every run. On 64-bit systems, the entropy is typically 28-30 bits for library randomization, making brute force impractical.
- Information leaks: A separate vulnerability that leaks memory addresses (e.g., a format string bug that prints stack values) can defeat both ASLR (reveals addresses) and canaries (reveals the canary value). This is why modern defenses add CFI (Control Flow Integrity) as a fourth layer — even if the attacker knows addresses, they cannot redirect execution to arbitrary gadgets because the CPU verifies that indirect branches target valid function entries.
Compare seccomp-BPF and SELinux as security mechanisms. If you were hardening a container running an untrusted workload, which would you use and why?
Compare seccomp-BPF and SELinux as security mechanisms. If you were hardening a container running an untrusted workload, which would you use and why?
- Seccomp-BPF (System Call Filter): Intercepts every syscall at the entry point and runs a BPF filter that decides allow/deny/kill based on the syscall number and (with some limitations) its arguments. It answers: “Can this process invoke this kernel API?” Seccomp cannot distinguish between files, network addresses, or process targets — if you allow
open(), the process can open any file. If you allowconnect(), it can connect to any address. - SELinux (Mandatory Access Control): Assigns security labels to every process, file, socket, and kernel object. A policy defines which labels can perform which operations on which other labels. It answers: “Can this specific subject access this specific object in this specific way?” SELinux can say “process with label httpd_t can read files with label httpd_content_t but cannot write to files with label etc_t.” This is far more granular than seccomp.
- Seccomp-BPF: Block all syscalls the container does not need. A web server does not need
mount,reboot,kexec_load,ptrace,init_module, orio_uring_setup. Docker ships a default seccomp profile that blocks about 60 dangerous syscalls. For untrusted workloads, I would create a custom profile that allowlists only the ~50 syscalls the application actually uses (determined by runningstraceduring testing). This shrinks the kernel attack surface enormously — most kernel CVEs are in obscure syscall handlers that a web server never touches. - SELinux (or AppArmor): Apply a policy that restricts what the container can access even with the allowed syscalls. The container process can call
open(), but SELinux ensures it can only open files in its designated directory. It can callconnect(), but SELinux restricts it to specific ports and network labels. This prevents a compromised container from reading/etc/shadow, connecting to the metadata service (a common cloud attack vector), or accessing the Docker socket.
open() allowed can read any file. SELinux without seccomp means a process can invoke dangerous syscalls (even if they fail due to policy, the syscall handler code still runs, potentially triggering kernel bugs).Follow-up: What is the performance overhead of running both seccomp-BPF and SELinux simultaneously?Seccomp-BPF adds 10-50 nanoseconds per syscall (running a small BPF program in the syscall entry path). SELinux adds 100-500 nanoseconds per security check (which happens on syscalls that access objects — file open, socket connect, etc.). For a web server making 10K syscalls per second, the combined overhead is roughly 0.5-5 milliseconds per second — negligible. For a storage-intensive application making 500K syscalls per second, the overhead is 25-250 milliseconds per second (2.5-25% of one core). The practical impact depends entirely on the syscall rate. For most workloads, the overhead is under 1% and invisible in application-level metrics. The security benefit far outweighs the cost.Explain the Spectre vulnerability. How does it differ from Meltdown, and why is Spectre considered harder to mitigate fully?
Explain the Spectre vulnerability. How does it differ from Meltdown, and why is Spectre considered harder to mitigate fully?
- Meltdown (CVE-2017-5754): Exploits the fact that on vulnerable Intel CPUs, speculative loads from kernel memory are not immediately stopped by the permission check. The CPU speculatively reads kernel data into a register, uses it to access a cache line (encoding the secret in a cache side channel), and then throws away the speculative result when the permission check fails. But the cache side channel remains — the attacker can probe which cache line was accessed and recover the kernel data. Meltdown crosses the user/kernel boundary and allows reading arbitrary kernel memory.
- Spectre (CVE-2017-5753 Variant 1, CVE-2017-5715 Variant 2): Exploits the CPU’s branch prediction. In Variant 1 (bounds check bypass), the attacker trains the branch predictor to predict that a bounds check will pass, then triggers speculative execution past the check with an out-of-bounds index. The speculative load accesses secret data and encodes it in the cache. In Variant 2 (branch target injection), the attacker poisons the Branch Target Buffer (BTB) to redirect speculative execution of an indirect branch to attacker-chosen code (“gadgets”) within the victim’s address space.
- Meltdown has a clean fix: KPTI (Kernel Page Table Isolation) unmaps the kernel from user-space page tables. If the kernel memory is not even mapped during user-space execution, the speculative load has nothing to read. The fix is at the OS level and is complete (with a 5-30% performance cost).
- Spectre crosses any trust boundary: Spectre does not require reading kernel memory. It can leak data between processes, between VMs, between JavaScript contexts in a browser, between a sandbox and its host. Any code running on the same CPU can potentially be a Spectre victim or attacker.
- Software mitigations are partial: Retpolines (replacing indirect branches with a return trampoline that defeats BTB poisoning) mitigate Variant 2 but add overhead to every indirect call. Array bounds masking (inserting an AND instruction after bounds checks to zero out speculative out-of-bounds accesses) mitigates Variant 1 but requires compiler changes and careful code auditing. Neither is a complete fix.
- New variants keep appearing: Spectre is a class of vulnerabilities, not a single bug. Spectre-v3a, Spectre-RSB, Spectre-BHB, and MDS (Microarchitectural Data Sampling) are all variations on the same theme. Each requires its own mitigation.
Threat model: someone gives you a binary you have to run, and you have to assume it is malicious. What boundaries does Linux give you, and how do you compose them?
Threat model: someone gives you a binary you have to run, and you have to assume it is malicious. What boundaries does Linux give you, and how do you compose them?
- Establish what the attacker should not be able to do. Before reaching for tools, define the boundary. “Cannot read
/etc/shadow” is different from “cannot exfiltrate any data” is different from “cannot persist a backdoor.” Threat modeling forces you to choose mechanisms that match the goal. - Apply user separation as the floor. Run the binary as a dedicated unprivileged user with no shell, no sudo entries, no group memberships beyond its own. This is the cheapest layer and rules out 80 percent of trivial attacks. Anyone who skips this layer because “I have stronger mechanisms above” loses if the stronger mechanisms have a bug.
- Drop capabilities to the minimum. Use
prctl(PR_CAPBSET_DROP)to drop the bounding set, setSECBIT_NOROOTto prevent file capabilities or setuid from re-elevating, and add only the capabilities the workload needs. For most workloads, the answer is zero capabilities. - Apply seccomp to shrink the kernel surface. Custom syscall whitelist generated from observed behavior. The kernel has 350+ syscalls; a typical workload uses 50-80. Blocking the rest closes off entire classes of kernel CVEs preemptively.
- Use namespaces to make the world smaller. Mount namespace with a chroot or pivot_root into a private rootfs. Network namespace with no interfaces (or just a loopback). PID namespace so the process cannot see or signal anything outside. User namespace with the workload mapped to a non-overlapping host UID, so even root-in-namespace is unprivileged on the host.
- Layer mandatory access control on top. SELinux or AppArmor profile that restricts what the workload can read or write even if it somehow got privilege. This is the layer that catches the bug in your seccomp profile.
- Cgroup limits for blast radius. Memory limit, PID limit, CPU quota, IO weight. These do not stop intrusions, but they bound the damage of fork bombs, memory hogs, and crypto-miners-as-payload.
- Audit what you cannot prevent. Even with all of the above, log every syscall through audit subsystem or eBPF tracing. The goal is detection within hours of a successful attack, not just prevention.
SECCOMP_RET_LOG instead of SECCOMP_RET_KILL, run the workload through realistic scenarios (not just happy path — include error handling, signal delivery, malloc growth), and watch /var/log/audit/audit.log for SECCOMP records. Each entry shows the syscall number that would have been killed; map those to names with ausyscall. After a clean observation window, flip default action to SECCOMP_RET_ERRNO(EPERM) for one more cycle (so the application can fail gracefully), then to SECCOMP_RET_KILL_PROCESS for hard enforcement. Tools like containerd-shim’s seccomp recorder, Falco, and kubectl-trace automate this loop.read() to the application are actually intercepted, validated, and either handled in Sentry or proxied to the host. This eliminates an entire class of risk: kernel CVEs in syscall handlers do not affect gVisor-sandboxed workloads because those handlers never execute on the host kernel for sandboxed traffic. The cost is real — gVisor adds 10-50 percent overhead on syscall-heavy workloads and is incompatible with some applications (Linux-namespace-specific tools, applications that mmap and then expect specific kernel behaviors). The cost is justified for workloads where you genuinely cannot trust the binary — shared CI runners, untrusted user code in PaaS, multi-tenant function execution. It is overkill for first-party microservices.- “Just put it in Docker.” Docker by default runs as root inside the container, with most capabilities, and a default-permissive seccomp profile. Docker is a packaging tool first; security depends on configuration that must be applied explicitly.
- “Use a VM.” VMs have a smaller attack surface than namespaces against most threat models, but the hypervisor still has CVE history (CVE-2017-2596 KVM, CVE-2020-29569 Xen). Saying “use a VM” without acknowledging hypervisor risk hand-waves the problem.
- “Drop capabilities and you are done.” Capabilities are necessary but not sufficient. A process with zero capabilities can still read every file world-readable, connect to localhost services, and exploit kernel bugs in syscalls that do not require capabilities.
- “Sandboxing and Workload Isolation” (Google production hardening guide) — the gVisor design rationale and threat model
- Jess Frazelle, “Hard multi-tenancy in Kubernetes” — pragmatic stack for untrusted workloads
- Linux source:
kernel/seccomp.c,kernel/user_namespace.c,security/security.cfor the LSM hook integration
Compare what seccomp, capabilities, namespaces, and Docker each contribute to container security. Where do they overlap, where do they fail to compose, and what should I expect each one to catch?
Compare what seccomp, capabilities, namespaces, and Docker each contribute to container security. Where do they overlap, where do they fail to compose, and what should I expect each one to catch?
- Capabilities answer: what privileged operations can this process invoke? Drop all capabilities and the process cannot bind low ports, change UIDs, mount filesystems, load kernel modules, ptrace others, or do anything else that historically required root. Capabilities do not restrict file access (DAC handles that) or syscall surface (seccomp handles that).
- Seccomp answers: what syscalls can this process make at all? Even without capabilities, a process can call hundreds of syscalls. Many have CVE history. Seccomp shrinks the kernel attack surface by blocking syscalls the workload does not need. It does not care about arguments deeply (only some support arg filtering), so it cannot say “open files only in /tmp.” It just says “you can or cannot call open at all.”
- Namespaces answer: what does this process see? Mount namespace = its own filesystem view. Network namespace = its own network stack. PID namespace = its own process tree. User namespace = its own UID/GID mapping. Namespaces isolate visibility and resource scope, not privilege. Two processes can be in the same namespace and one can attack the other; namespaces only protect across the boundary.
- Docker is the orchestration that wires these together. A Docker container is, mechanically, a process tree with namespaces, a default seccomp profile, dropped capabilities (most are off by default), an AppArmor or SELinux profile, and cgroup limits. Docker is not a new isolation mechanism — it is a configuration that combines existing kernel mechanisms.
- Where they fail to compose: seccomp filters by syscall number, but a syscall you allow can transitively reach functionality you blocked (the
mprotect-via-printf issue). Namespaces leak through/proc,/sys, kernel keyrings, and shared kernel data structures. User namespaces have escalation paths through misconfigured uid_map. Capabilities have surprising scopes —CAP_SYS_ADMINis “nearly root” because dozens of operations gate on it. Combining all four is necessary; each individually has gaps the others fill.
/proc/self/fd, and a malicious container could traverse those FDs to reach the host filesystem. None of the standard hardening primitives caught this because the attack did not violate any one mechanism’s contract — it exploited the gap between them. The fix was at the runtime level: runc closes all FDs before exec, a behavior that should have been there all along.--cap-drop=ALL --cap-add=.... The Docker maintainers chose conservative defaults so docker run would just work for as many users as possible, accepting a less-defensive baseline as the cost. For production, your image build or orchestration layer should override this default.SECCOMP_FILTER_FLAG_TSYNC synchronizes a seccomp filter across all threads in the process at install time, ensuring no thread escapes the filter. Without it, a multithreaded process can install a filter on the calling thread but other threads keep running unfiltered until they call prctl(PR_SET_SECCOMP) themselves. For a single-threaded program this is fine; for anything threaded (which is most modern code), TSYNC is mandatory or you have a race where a thread spawned during filter installation never gets the filter. The 2017 CVE-2017-2671 in QEMU is one example of this exact race being exploited.- “Containers are basically VMs.” They are emphatically not. A VM has a hardware-virtualized hypervisor between guest and host kernel; a container shares the host kernel directly. Container escapes target host kernel bugs; VM escapes target hypervisor bugs (rarer, smaller surface).
- “Seccomp blocks system calls and that is enough.” Seccomp does not see filesystem paths or network addresses. A process with
openallowed can read every file your DAC allows; a process withconnectallowed can reach every IP your network namespace permits. - “If I drop all capabilities I am safe.” Many CVEs do not need capabilities. Reading sensitive files via standard DAC, exploiting kernel bugs in syscalls that do not require privilege, and lateral movement through the container’s mount namespace are all capability-free.
- “Container Security” by Liz Rice — the cleanest book-length tour of the kernel primitives and how Docker/Kubernetes wire them
- LWN article: “Capabilities for system calls” (Mickael Salaun) — why caps and seccomp are complementary
- runc CVE-2024-21626 writeup — a real-world example of compositional failure
- Linux source:
Documentation/userspace-api/seccomp_filter.rst,Documentation/security/credentials.rst
Walk me through how Spectre actually works -- from the CPU's perspective and from the attacker's. Then explain what mitigations the kernel applies and what you give up.
Walk me through how Spectre actually works -- from the CPU's perspective and from the attacker's. Then explain what mitigations the kernel applies and what you give up.
- The CPU’s perspective: speculation as a performance feature. A modern CPU does not wait for a branch’s condition to resolve before fetching, decoding, and executing instructions on one of the predicted paths. Branch predictors — including the Pattern History Table for direct branches and the Branch Target Buffer (BTB) for indirect branches — predict where execution is going. The CPU executes speculatively, retains results in the Reorder Buffer, and either commits them (prediction correct) or discards them (prediction wrong). The trick is that “discards them” is not perfect: side effects on microarchitectural state — cache lines loaded, branch predictor state updated — persist even when the architectural result is rolled back.
- The attacker’s perspective: turning microarchitectural side effects into a data leak. Spectre Variant 1 (bounds check bypass): the attacker trains the branch predictor to expect a bounds check to pass, then triggers the speculative path with an out-of-bounds index. The speculative load reads secret memory, uses the secret as an index into a probe array, and brings a specific cache line into L1. The architectural result is rolled back, but the cache state is not. The attacker times accesses to the probe array; the line that hits in cache encodes the secret byte. With this primitive, the attacker reads memory at the rate of about 10-100 KB/sec.
- Spectre Variant 2 (branch target injection): poisoning indirect branches. The attacker pollutes the BTB with branch targets that, when used speculatively by the victim, redirect speculative execution to attacker-chosen code — “gadgets” — in the victim’s address space. Now the speculative-execution-and-cache-side-channel pattern can read across security boundaries (kernel space, other VMs, browser sandboxes).
- Kernel mitigations: per-variant. Variant 1 mitigated with array bounds masking (
array_index_nospec) and LFENCE / speculation barriers in kernel hot paths — compiler and code review job, painful and incomplete. Variant 2 mitigated with retpolines on x86 (replacing indirect branches with a return trampoline that defeats BTB poisoning) and IBRS / IBPB / STIBP CPU features (clearing predictor state at boundary crossings). Cross-process (and cross-VM) protection via core scheduling — never schedule untrusted SMT siblings on the same physical core. - What you give up. Retpolines add 5-30 percent overhead to indirect-call-heavy workloads (interpreters, VM monitors, system call entry). KPTI (which mitigates Meltdown but is part of the same family) costs 5-30 percent on syscall-heavy workloads, especially on older CPUs without PCID. Disabling SMT for security on multi-tenant hosts halves logical core count. The total cost on a Skylake-era Xeon running a syscall-heavy workload is non-trivial — often 10-25 percent throughput loss compared to a fully-mitigation-disabled baseline.
jmp *%rax) consults the BTB for prediction, which the attacker has poisoned. Retpoline replaces it with a sequence: push the target onto the stack, then ret. The CPU’s return address predictor (Return Stack Buffer, RSB) is used instead of the BTB for ret. The attacker cannot easily poison the RSB because it is filled by call instructions, which the attacker controls less. The speculation that does happen lands in an infinite loop (pause; jmp self), so even if the predictor is wrong, the speculative path does not perform any useful work for an attacker. The cost is a few extra instructions per indirect call. On AMD CPUs and newer Intel CPUs with eIBRS (Enhanced Indirect Branch Restricted Speculation), retpoline is replaced with a hardware mode flag that gives equivalent protection at lower overhead.mitigations=off or specific flags like nopti, spectre_v2=off). The risk you accept: an unknown future CVE that uses speculation to escape userspace, or a supply-chain compromise in a trusted dependency. Most production environments cannot make this tradeoff because the trust assumptions do not actually hold; HPC and game servers can. Document the decision explicitly so the next operator does not assume mitigations are on.- “Spectre and Meltdown are the same thing.” They share a primitive (cache side channel after speculation) but differ in trust boundary and mitigation profile. Conflating them suggests you have read the headline but not the technical writeup.
- “Just patch your CPU microcode.” Microcode updates are part of the mitigation but cannot fix Spectre fully because Spectre is a behavior, not a bug. Software mitigations remain mandatory.
- “Disable Hyper-Threading and you are safe.” Disabling SMT helps against L1TF and MDS variants where SMT siblings share microarchitectural state, but does nothing for cross-process Spectre on the same core. It is one mitigation, not the answer.
- The original Spectre paper: Kocher et al., “Spectre Attacks: Exploiting Speculative Execution” (2018, USENIX Security)
- LWN article: “The current state of kernel page-table isolation” — comprehensive KPTI walkthrough
- Intel “Speculative Execution Side Channel Mitigations” white paper — the vendor’s view
- Linux source:
arch/x86/kernel/cpu/bugs.c,arch/x86/include/asm/nospec-branch.h
Next: Boot Process & Initialization →