Containers & Virtualization
Isolation is the core requirement of multi-tenant cloud computing. Whether you are running a SaaS platform or a microservices cluster, you must ensure that processes are contained, resources are metered, and security boundaries are enforced. Modern systems achieve this through two distinct paths: OS-level virtualization (Containers) and Hardware-level virtualization (VMs).1. Container Internals: The Linux “Trio”
A container is not a “thing” in the Linux kernel. It is a user-space abstraction built using three primary kernel features: Namespaces, Control Groups (cgroups), and Union Filesystems.1.1 Namespaces: The Illusion of Isolation
Namespaces wrap global system resources in an abstraction that makes it appear to the processes within the namespace that they have their own isolated instance of the resource.Deep Dive: PID Namespace
The PID namespace creates a hierarchical process view where each namespace has its own PID 1.- First process in namespace becomes PID 1
- If PID 1 exits, kernel kills all processes in namespace
- Parent namespace can see child processes with their “real” PIDs
/procshows only processes in current namespace (with mount namespace)
Deep Dive: Network Namespace
Network namespaces isolate the network stack: devices, routing tables, firewall rules, sockets.Deep Dive: Mount Namespace
Mount namespaces isolate the filesystem mount points.Deep Dive: User Namespace
User namespaces allow mapping UIDs/GIDs, enabling rootless containers.Deep Dive: IPC Namespace
IPC namespaces isolate System V IPC objects and POSIX message queues.Deep Dive: UTS Namespace
UTS namespaces isolate hostname and domain name.Deep Dive: Time Namespace
Time namespaces (Linux 5.6+) allow different boot times and monotonic clocks.The pivot_root vs chroot
While chroot only changes the root directory for path resolution, it is insecure (processes can “break out” via .. or file descriptor trickery). Containers use pivot_root, which moves the entire mount namespace to a new root and removes access to the old one, providing a true filesystem jail.
1.2 Cgroups: Resource Metering and Limiting
If Namespaces provide isolation (what you see), Cgroups provide containment (what you can use).- Cgroups v1 (Legacy): Multiple hierarchies. A process could be in one group for CPU and a completely different group for Memory. This led to massive complexity and performance issues.
- Cgroups v2 (Modern/Unified): A single hierarchy. Every process belongs to exactly one cgroup in a unified tree. This allows for better resource accounting (e.g., attributing page cache writeback to the specific cgroup that caused the dirty pages).
Key Controllers:
CPU Controller:1.3 OverlayFS: The Layered Filesystem
Containers use Union Filesystems (like OverlayFS) to provide a writable layer on top of read-only image layers.- LowerDir: Read-only layers (the Docker image).
- UpperDir: The writable layer where changes are stored.
- MergedDir: The unified view presented to the container.
- Copy-on-Write (CoW): When a container modifies a file in the LowerDir, the kernel first copies it to the UpperDir before applying the change.
2. Virtualization: Emulating the Machine
Virtual Machines (VMs) take the isolation boundary down to the hardware level. Instead of sharing a kernel, they share the physical CPU and Memory.2.1 The Hypervisor (VMM)
The Virtual Machine Monitor (VMM) is the software that manages guest execution.- Type 1 (Bare Metal): Runs directly on hardware (Xen, ESXi).
- Type 2 (Hosted): Runs as an app on a host OS (KVM, VirtualBox). Note: KVM is unique because it turns the Linux kernel itself into a Type 1 hypervisor.
2.2 Hardware-Assisted Virtualization (VT-x / AMD-V)
Early virtualization used “Binary Translation” to replace privileged instructions. Modern CPUs handle this in hardware:- VMX Root Mode: The hypervisor runs here (full privileges).
- VMX Non-Root Mode: The guest OS runs here. If the guest tries to execute a privileged instruction (like
HLTor modifyingCR3), the CPU triggers a VM Exit, trapping into the hypervisor to handle the event.
VMCS (Virtual Machine Control Structure)
The VMCS is a memory block that stores the “state” of a virtual CPU (registers, control bits). When switching from VM A to VM B, the hypervisor swaps the VMCS pointers.2.3 Memory Virtualization: EPT and NPT
In a VM, there are three types of addresses:- Guest Virtual (GV)
- Guest Physical (GP)
- Host Physical (HP)
3. The Middle Ground: MicroVMs
Plain containers have a large attack surface (thousands of syscalls). Plain VMs are slow and heavy. MicroVMs (like Firecracker) bridge the gap.Firecracker Architecture
- Minimalism: Removes all non-essential devices (no VGA, no USB, no sound).
- VirtIO: Uses paravirtualized drivers for network and disk, avoiding the overhead of emulating real hardware registers.
- Jailer: Firecracker itself runs inside a container (Namespaces + Cgroups) to provide “Defense in Depth.”
- Performance: Can boot a Linux kernel in under 125ms and run thousands of instances on a single host.
4. Comparison: When to Use What?
5. Docker Internals: Putting It All Together
Docker is a high-level container runtime that orchestrates namespaces, cgroups, and OverlayFS.6. Interview Deep Dive: Senior Level
Q1: How does 'User Namespaces' improve container security?
Q1: How does 'User Namespaces' improve container security?
CLONE_NEWUSER) allow a process to have UID 0 (root) inside the container while being a non-privileged UID (e.g., 1000) on the host.Security Improvement:- Some operations still require host root (mounting certain filesystems)
- File ownership can be confusing (files created by container appear owned by high UIDs on host)
- Not all containers work with user namespaces (especially those requiring true root)
Q2: Explain the difference between cgroups v1 and v2 and why v2 is better
Q2: Explain the difference between cgroups v1 and v2 and why v2 is better
-
Multiple Hierarchies:
- Each controller (cpu, memory, io) has its own hierarchy
- A process can be in
/sys/fs/cgroup/cpu/groupAand/sys/fs/cgroup/memory/groupB - Impossible to do unified resource accounting
-
Writeback Ambiguity:
- Process in cgroup A writes to page cache
- Page cache writeback happens later
- Which cgroup gets charged for the disk I/O?
- v1: Charged to whoever triggers writeback (wrong!)
-
No Delegation:
- Can’t safely give non-root users control over cgroups
- Security issues with nested hierarchies
-
Single Hierarchy:
- One tree, all controllers
- Process location is the same for all resources
- Enables proper delegation and accounting
-
Proper Attribution:
- Tracks which cgroup dirtied pages
- I/O charged correctly even if writeback delayed
-
Pressure Stall Information (PSI):
- Built-in resource pressure metrics
- Can detect when cgroup is starved
Q3: How does Docker implement network isolation and connectivity?
Q3: How does Docker implement network isolation and connectivity?
Q4: What is a 'VM Exit' and why is it expensive?
Q4: What is a 'VM Exit' and why is it expensive?
-
EPT (Extended Page Tables):
- Guest can change CR3 without VM exit
- Hardware handles GVA → GPA → HPA translation
-
APIC Virtualization:
- Virtual APIC page in guest memory
- Most interrupt operations happen without exits
-
VirtIO:
- Paravirtualized drivers
- Shared memory rings reduce I/O exits
Q5: Explain the 'Nested Virtualization' problem
Q5: Explain the 'Nested Virtualization' problem
-
Development/Testing:
- Test hypervisor code
- CI/CD pipelines testing VMs
-
Cloud Services:
- Kubernetes on cloud VMs
- CI runners in cloud
-
Education:
- Teaching virtualization
- Lab environments
- Production databases
- High-performance computing
- Latency-sensitive applications
Q6: How does OverlayFS implement copy-on-write for containers?
Q6: How does OverlayFS implement copy-on-write for containers?
- Shared base layers save disk space
- Fast container startup (no copying)
- Efficient use of cache (shared pages)
- First write to file triggers copy-up (can be slow for large files)
- Many layers slow down lookup
- Whiteouts can accumulate (use
docker system prune)
Q7: What are the security implications of sharing the kernel in containers vs VMs?
Q7: What are the security implications of sharing the kernel in containers vs VMs?
- Faster startup and lower overhead
- Easier management
-
Kernel Exploits:
-
Large Attack Surface:
-
Resource Exhaustion:
-
Information Leakage:
-
Strong Isolation:
-
Smaller Attack Surface:
-
Different Kernels:
Q8: How do hypervisors implement device emulation vs paravirtualization?
Q8: How do hypervisors implement device emulation vs paravirtualization?
- Pros: No guest modification, runs any OS
- Cons: Slow, many VM exits
- Pros: Fast, few VM exits
- Cons: Requires guest support (modified drivers)
- Use paravirt for performance-critical devices (disk, network)
- Use emulation for legacy devices (VGA, PS/2)
- Gradually reduce emulation over time
6. Namespaces & Cgroups: A Single Process’s Perspective
What does a process actually “see” when it’s containerized? Here’s the view from inside:What Changes for the Process
Inspecting Your Own Namespace
The Process Doesn’t Know It’s Contained
Key Insight: Syscalls Are Virtualized
Every syscall that returns information about the system goes through namespace translation:Production Caveats and Patterns
The container ecosystem is full of footguns that only fire under load, in production, after months of working fine. Below are the four pitfalls that have generated the most incidents in real engineering organizations, paired with the patterns that defuse them.Senior Interview Questions
Containers vs VMs -- when do you choose which, and what does 'isolation' really mean here?
Containers vs VMs -- when do you choose which, and what does 'isolation' really mean here?
- State the isolation boundary first. A VM virtualizes hardware — the hypervisor (KVM, Hyper-V, Xen) traps privileged instructions, presents virtual CPUs and devices, and the guest runs its own kernel. A container shares the host kernel and uses namespaces plus cgroups to restrict what a process sees and how much it can consume. The boundary determines the threat model: a VM contains a guest-kernel exploit; a container does not.
- Describe the cost dimensions. VMs pay for boot time (seconds to tens of seconds for a full OS), per-instance memory (50-500 MB just for the guest kernel and userspace), and VM-exit overhead on every privileged operation. Containers boot in milliseconds, share the host kernel page cache, and have near-native syscall latency. On a typical 64 GB server, you fit 10-30 VMs or hundreds-to-thousands of containers.
- Map workload to choice. Multi-tenant code execution (Lambda, code sandboxes, customer-supplied workloads) wants a VM-grade boundary. Internal microservices owned by your org want containers — you trust the code, you want the density. Long-running stateful services with strict noisy-neighbor isolation (databases on shared hardware) sit in the middle: VMs or microVMs.
- Acknowledge the middle ground. Firecracker and Cloud Hypervisor are minimal VMMs that boot a microVM in ~125 ms with ~5 MB overhead — AWS Lambda and Fly.io use them precisely because they want VM isolation at container density. gVisor goes the other way: a user-space kernel that intercepts syscalls before they reach the host kernel, reducing the kernel attack surface at a 2-5x I/O perf cost.
- Name your default. “I default to containers for everything I own and trust, microVMs (Firecracker) for anything where the threat model includes hostile guest code, and full VMs only when I need a different OS or hardware-level features (nested virtualization, GPU passthrough).”
--security-opt no-new-privileges and a strict seccomp profile — safe enough? No. You have closed common privilege-escalation paths, but you have not closed the kernel attack surface. CVE-2022-0492 (cgroups release_agent), CVE-2022-0185 (filesystem mount), and CVE-2023-0386 (overlayfs) all let a non-root container process escape to the host on default-configured systems. Seccomp narrows the syscall surface but does not eliminate it. Use a microVM if the code is genuinely hostile.- “Containers are just lightweight VMs.” Wrong on both technical and security grounds. Different isolation boundary, different threat model, different operational profile. This phrasing signals the candidate has not internalized why CVE-2019-5736 was possible at all.
- “VMs are always more secure.” Not always. A poorly-configured VM (default credentials, exposed management interface, unpatched hypervisor) is less secure than a hardened container. Isolation is necessary but not sufficient.
- “Use Kubernetes; it handles all this.” Kubernetes is an orchestrator, not an isolation strategy. By default, K8s pods on the same node share the kernel. Multi-tenant K8s requires explicit decisions: gVisor RuntimeClass, Kata Containers, dedicated node pools per tenant.
- Aqua Security CVE write-up on runc CVE-2019-5736 — the canonical example of a container breakout via shared filesystem semantics.
- Firecracker design paper (NSDI 2020), “Firecracker: Lightweight Virtualization for Serverless Applications.”
- “Container Security” by Liz Rice (O’Reilly) — the standard reference for production container isolation.
Walk me through how Docker actually starts a container, namespace by namespace.
Walk me through how Docker actually starts a container, namespace by namespace.
- Start with the layered architecture.
docker runtalks to dockerd over a Unix socket. dockerd parses the request and delegates to containerd via gRPC. containerd resolves the image (pulling layers if needed), prepares an OCI runtime spec (a JSON config describing the container), and shells out to runc. runc is the piece that actually creates the namespaces. - The runc dance. runc forks itself. The child calls
unshare()(or passesCLONE_NEW*flags toclone()) to create new namespaces in a single transition: PID, mount, UTS, IPC, net, user, cgroup. Order matters — user namespace must be set up first so subsequent namespaces are owned by it. The parent stays in the host namespace and writes the new PID into the appropriate cgroup files. - Filesystem setup. Inside the new mount namespace, runc bind-mounts the OverlayFS merged directory (lower = read-only image layers, upper = writable container layer, work = OverlayFS bookkeeping) to a temporary path, then
pivot_roots into it. Old root is unmounted./procand/sysget fresh mounts inside the new mount namespace — this is what makescat /proc/1/statusshow the containerized process as PID 1. - Resource caps. The cgroup is configured before the workload starts:
memory.max,cpu.max,pids.max,io.maxare all written to the cgroup directory. The process is added to the cgroup viacgroup.procs. Once added, the kernel enforces limits on every allocation and scheduling decision for that process and its descendants. - Security hardening. runc applies the seccomp BPF filter (~50 syscalls blocked by default profile), drops capabilities (default keeps ~14 of 40+ capabilities), applies AppArmor or SELinux label, and finally
execves the actual entrypoint. At this point, the process is the container. - Detach. runc exits, leaving the entrypoint reparented to a
containerd-shimprocess. The shim holds the container’s TTY and exit code so dockerd can crash and the container survives.
rtnl_lock (the global netlink lock) became a bottleneck — veth creation serialized across cores. Fix was to switch to ipvlan (which is lock-free per-namespace) and to batch container creation to avoid thundering-herd lock contention. The lesson: namespace creation is not free, and the cost is concentrated in specific kernel locks.docker run --pid=host exist, and when is it dangerous? It tells runc to skip creating a PID namespace — the container shares the host’s PID namespace. Useful for debugging tools (a sidecar that needs to kill -9 a host process) or process supervisors. Dangerous because a process inside the container can now see and signal every process on the host; if the container has CAP_KILL or CAP_SYS_PTRACE, that is a full escape primitive.docker exec different from docker run? exec does not create new namespaces. It uses setns(2) to enter the existing container’s namespaces (one syscall per namespace, with the target file descriptor from /proc/PID/ns/*), then execves the new command. The new process inherits all the container’s restrictions but is not its child — which is why execed processes do not show up under PID 1 in some ps views and require explicit handling for signal forwarding.- “Docker creates a lightweight virtual machine.” No. Docker creates namespaces and cgroups; there is no VM, no hypervisor, no separate kernel.
- “Containers are just chroot jails.” chroot only changes the filesystem root. Namespaces additionally isolate process IDs, network, IPC, hostname, users, and cgroup view. Containers are chroot plus seven other forms of isolation, plus resource limits.
- “Docker uses LXC.” It used to (in 2013-2014). Since libcontainer (now runc), Docker has its own implementation. LXC is a separate project with different design choices (system containers vs application containers).
- Liz Rice, “Containers from Scratch” talk (Container Camp 2017) — builds a container in ~100 lines of Go.
- OCI Runtime Specification (github.com/opencontainers/runtime-spec) — the actual contract between containerd and runc.
- “What Have Syscalls Done for You Lately?” by Jessie Frazelle — a tour of the kernel features Docker exercises.
gVisor, Firecracker, Kata Containers -- compare the security trade-offs and pick one for an untrusted code workload.
gVisor, Firecracker, Kata Containers -- compare the security trade-offs and pick one for an untrusted code workload.
- Frame the question as ‘what is the attack surface’. Plain Docker exposes the full Linux syscall ABI (~340 syscalls, ~70 typically allowed by default seccomp). Any kernel bug reachable through those syscalls is a potential escape. The three alternatives reduce the attack surface in different ways.
- gVisor (runsc). A user-space kernel called Sentry implements the Linux syscall interface in Go. Container syscalls go to Sentry; Sentry only makes ~20 syscalls to the host kernel. Attack surface drops by an order of magnitude. Cost: every syscall traverses Sentry’s user-space implementation, so I/O-heavy workloads see 2-5x slowdowns. Some uncommon syscalls or kernel features are not implemented (io_uring, some namespaces). Used by Google for App Engine and Cloud Run.
- Firecracker. Each container gets a lightweight VM with a minimal Linux kernel, a virtio-only device model, and a ~50k-line Rust VMM. The host kernel attack surface reduces to KVM plus virtio. Boot time ~125 ms, memory overhead ~5 MB per microVM. Syscall-compatible (it is real Linux), supports any syscall the guest kernel supports. Cost: per-VM kernel memory, VM-exit overhead, and you now manage a kernel image lifecycle. Used by AWS Lambda, AWS Fargate, Fly.io.
- Kata Containers. Same idea as Firecracker — per-container VM — but using QEMU (or Cloud Hypervisor) and integrating directly with Kubernetes via a RuntimeClass. Compatible with full OCI semantics, supports more device types (GPUs via VFIO). Slightly heavier than Firecracker (~50-100 ms boot, ~30 MB memory) but more flexible.
- Recommend with reasoning. “For untrusted code execution at scale — think a Lambda-style platform, a code-runner SaaS, or sandboxed CI — I would pick Firecracker. The threat model is ‘guest is hostile,’ and Firecracker gives me VM-grade isolation with container-grade density and start time. gVisor is a strong alternative when I cannot run my own kernel image (managed K8s without RuntimeClass support) and the workload is not I/O-bound.”
nogo (Go without GC for the syscall hot path) and platform-specific optimizations to claw some of it back.- “They are all the same thing — secure containers.” They use fundamentally different mechanisms. gVisor intercepts syscalls in user space; Firecracker and Kata run a real guest kernel in a VM. The security properties and performance profiles are not interchangeable.
- “Just use seccomp.” Seccomp narrows the syscall surface but does not change the trust boundary. The remaining allowed syscalls still execute in the host kernel. Seccomp is a hardening layer, not an isolation strategy.
- “VMs are always slower.” Not for the metrics that matter at scale. Firecracker boots faster than many container runtimes (~125 ms vs ~200-500 ms for full Docker). Per-syscall overhead is comparable to native for compute-bound workloads. The VM tax is real but smaller than people assume.
- Firecracker NSDI 2020 paper — Agache et al., “Firecracker: Lightweight Virtualization for Serverless Applications.”
- gVisor docs (gvisor.dev) — specifically the “Performance Guide” page which is honest about where gVisor is slow.
- Kata Containers architecture overview at katacontainers.io — explains the shim-v2 model.
7. Advanced Practice
- Manual Namespace Build: Use the
unsharecommand to create a shell with its own network and PID namespace. Try to ping the host. - Cgroup Stress Test: Create a cgroup v2 with a 100MB memory limit. Run a program that allocates 200MB and observe the kernel’s OOM killer logs in
dmesg. - VirtIO Analysis: Run a KVM guest and use
lspciinside the guest to identify which devices are usingvirtiodrivers vs. emulated hardware.
Next: OS Security & Hardening →
Interview Deep-Dive
A colleague says 'containers are lightweight VMs.' Correct this misconception, and explain the security implications of the difference.
A colleague says 'containers are lightweight VMs.' Correct this misconception, and explain the security implications of the difference.
- Containers and VMs have fundamentally different isolation boundaries. A VM runs its own kernel on virtualized hardware — the hypervisor (KVM, Xen, Hyper-V) provides each VM with a virtual CPU, virtual memory, and virtual devices. The guest OS has no direct access to the host kernel. A container, on the other hand, shares the host kernel. It is just a regular process (or group of processes) with restricted views of system resources via namespaces and restricted resource usage via cgroups.
- The security implication is significant: a kernel vulnerability (like a privilege escalation in a syscall handler) can allow a container to escape to the host. In a VM, the guest kernel vulnerability stays contained because the guest cannot directly call host kernel code — it must go through the hypervisor, which is a much smaller attack surface. This is why multi-tenant public clouds (AWS, GCP) use VMs, not containers, as the primary isolation boundary for customer workloads.
- Containers add defense-in-depth with seccomp (restricting which syscalls a container can make), AppArmor/SELinux (mandatory access control profiles), dropped capabilities (containers typically run without CAP_SYS_ADMIN), and user namespaces (mapping container root to a non-root host UID). But all of these are enforced by the shared host kernel, so a kernel bug can bypass all of them simultaneously.
- The middle ground is gVisor (which interposes a user-space kernel that handles syscalls before they reach the real kernel) and Kata Containers / Firecracker (which run each container inside a lightweight VM). AWS Lambda uses Firecracker MicroVMs — each function invocation gets its own VM with a minimal Linux kernel that boots in about 125ms.
--pid=host flag), which breaks isolation and is a serious security risk.Explain how cgroups v2 memory limits work internally. What happens when a container hits its memory limit -- is it always an OOM kill?
Explain how cgroups v2 memory limits work internally. What happens when a container hits its memory limit -- is it always an OOM kill?
- In cgroups v2, the
memory.maxfile sets the hard memory limit for a cgroup. The kernel tracks every page allocated by processes in the cgroup: anonymous pages (heap, stack), file-backed pages (page cache), kernel memory (slab, socket buffers), and swap (ifmemory.swap.maxis set). - When a process in the cgroup tries to allocate memory and the cgroup’s usage is at
memory.max, the kernel first tries to reclaim memory. It invokes the cgroup-aware reclaimer, which scans the cgroup’s LRU lists and evicts reclaimable pages — file-backed pages can be dropped (clean) or written back (dirty), and anonymous pages can be swapped out if swap is available. - If reclamation succeeds in freeing enough memory, the allocation proceeds and there is no OOM kill. This is actually the common case — the page cache grows until it fills the memory limit, then the kernel evicts old cached pages to make room for new allocations.
- OOM kill only happens when reclamation fails — there is no more reclaimable memory (all pages are anonymous and there is no swap, or swap is also full). At that point, the cgroup-level OOM killer selects a process within the cgroup to kill. Crucially, it will NOT kill processes outside the cgroup, which is the whole point of containerized memory limits.
- There is also
memory.high, which is a throttling threshold (not a hard limit). When usage exceedsmemory.high, the kernel applies memory pressure by slowing down allocations (the process is forced to do direct reclaim, which is slow). This provides backpressure before hitting the hard limit, giving the application a chance to reduce its memory footprint.
free command?Container monitoring tools (like docker stats or cAdvisor) report cgroup-level memory usage, which includes the page cache attributed to that cgroup. The page cache is reclaimable, so it is not “used” in the same sense as heap memory, but it counts against the cgroup’s limit. This is why a container might show 90% memory usage while the application inside thinks it is only using 200MB of heap — the rest is kernel page cache from file I/O. The memory.stat file in the cgroup filesystem breaks this down into anon, file, shmem, etc. When diagnosing memory issues, always check memory.stat rather than just memory.current.You need to run an untrusted workload in production. Compare the isolation guarantees of Docker containers, gVisor, and Firecracker MicroVMs. Which would you choose and why?
You need to run an untrusted workload in production. Compare the isolation guarantees of Docker containers, gVisor, and Firecracker MicroVMs. Which would you choose and why?
- Docker containers (with default settings) provide namespace isolation, cgroup resource limits, seccomp syscall filtering (~300 blocked syscalls), and dropped capabilities. The attack surface is the full Linux kernel syscall interface — roughly 70 syscalls are allowed by default. A kernel zero-day in any of those 70 syscalls can escape the container. In practice, this is good enough for trusted workloads (your own code) but risky for untrusted code.
- gVisor (runsc) interposes a user-space kernel called Sentry that implements the Linux syscall interface. The untrusted process’s syscalls go to Sentry, not the real kernel. Sentry only makes a small subset of syscalls to the host kernel (around 20), massively reducing the attack surface. The trade-off is performance: every syscall goes through Sentry’s user-space implementation, which adds latency. I/O-heavy workloads can see 2-5x slowdowns. gVisor also does not support every Linux syscall perfectly, so some applications may not run correctly.
- Firecracker MicroVMs give each workload its own lightweight VM with a minimal Linux kernel. The host kernel attack surface is reduced to KVM (the hypervisor) and a small set of virtio device emulations. Firecracker itself is written in Rust with a minimal device model (~50k lines of code), so the attack surface is tiny. Boot time is about 125ms, and memory overhead is about 5MB per VM. The trade-off is that you need to manage a full (minimal) kernel per workload, and there is overhead from the virtualization layer (EPT/SLAT page table walks, VM exits).
- For untrusted workloads, I would choose Firecracker if latency and syscall compatibility matter (like running arbitrary user-submitted code, which is what AWS Lambda does), or gVisor if the workload is I/O-light and I want strong isolation without the operational complexity of managing VM images. I would never use plain Docker for truly untrusted code.