Representative interview topic

Linux Interview: How Do Containers Isolate Processes and Resources?

GeneralHard
Offer.cc Editorial TeamPublished Updated

Question

On a cgroup v2 Linux host, container A sees its application as PID 1 and has a private hostname, mount table, and network stack. It is configured for 512 MiB of memory, an average of 0.5 CPU, and at most 128 tasks. Explain how a runtime constructs this boundary with namespaces, a root filesystem, and cgroups; what remains shared with the host; what happens when each limit is reached; how user namespaces, capabilities, seccomp, and an LSM strengthen the boundary; and how you would verify every claim from the host and the container.

Prompt and Applicable Context

On a Linux host using cgroup v2, container A has these exercise constraints:

ConstraintConcrete cgroup v2 value
Memorymemory.max = 536870912 bytes, or 512 MiB
CPUcpu.max = 50000 100000, or up to 50 ms every 100 ms
Task countpids.max = 128

Inside the container, the application sees itself as PID 1, a container hostname, its own mount table, and its own network interfaces. Explain how a runtime creates that environment, where isolation actually comes from, and how limit failures appear. Then cover the shared-kernel security boundary and prove the configuration with observable kernel state rather than a successful docker run alone.

This question applies to Linux, platform, SRE, DevOps, infrastructure, cloud, security, and backend roles. The values are interview constraints, not recommended defaults. “Container” refers to a Linux OCI-style process container; VM-backed container products add a different isolation layer.

What the Interviewer Is Evaluating

The first test is whether the candidate can replace the phrase “lightweight VM” with an accurate process model. A Linux container is a process tree on the host. Namespaces change the global resources that those processes can see: PIDs, mounts, network objects, IPC objects, host identity, user IDs, cgroup paths, and optionally clocks. A root filesystem and a private mount view supply the userspace image. None of those creates a second kernel.

The second test is separating visibility from resource control. Namespaces answer “which instance can this process observe or modify?” Cgroups answer “how much can this group consume, and how is it accounted?” A PID namespace does not cap the number of tasks. A cgroup PID limit does not hide host processes. A mount namespace changes the mount view, while the rootfs supplies files. chroot alone is neither a complete container nor a security boundary.

The third test is deriving behavior from exact cgroup v2 files. memory.max is a hard memory boundary that can lead to a cgroup-local OOM kill after reclaim fails. cpu.max is bandwidth control: after consuming 50 ms in a 100 ms period, runnable work is throttled until quota is available; it is not killed. pids.max rejects a violating fork() or clone() with EAGAIN. The candidate should name the event counters that distinguish these outcomes.

The fourth test is security judgment. Containers share the host kernel, so namespaces and cgroups do not equal a VM boundary. User-ID mapping, a small capability set, no_new_privs, seccomp, an LSM such as AppArmor or SELinux, read-only or masked mounts, and limited devices reduce attack surface. A privileged container, host PID or network namespace, Docker socket, or broad host mount can deliberately remove important boundaries.

Finally, the interviewer wants a verification plan. The answer should compare namespace inode identities, UID/GID mappings, cgroup membership and controller files, process capabilities, seccomp state, mount propagation, network interfaces, and failure counters from both sides of the boundary.

Questions to Clarify Before Answering

  • Which runtime and container mode are in scope? An OCI runtime on Linux is assumed. Rootless mode, user-namespace remapping, privileged mode, and VM-backed sandboxes change the trust boundary.
  • Does “0.5 CPU” mean quota or relative weight? Here it means a bandwidth cap of 50,000 microseconds per 100,000-microsecond period. cpu.weight only changes relative share during contention.
  • What does the 512 MiB include? cgroup v2 accounts major users such as anonymous memory, page cache, kernel structures, and socket buffers, but not every host resource is controlled by this single file.
  • Does 128 mean processes or kernel tasks? The pids controller uses kernel task IDs, so threads also consume the budget. That detail matters for highly threaded runtimes.
  • Is swap enabled and separately limited? memory.max and memory.swap.max are different controls. The prompt fixes only the memory limit, so swap policy must be inspected rather than assumed.
  • Which namespaces are actually configured? OCI configuration can create a namespace, join an existing namespace by path, or omit the type and inherit the runtime's namespace. Missing one is a real boundary change.
  • What is the threat model? Multi-tenant hostile workloads may require a VM or microVM boundary in addition to process isolation. A trusted internal workload may accept a different balance.

30-Second Answer Framework

“A Linux container is a host process tree started against a rootfs, placed in selected namespaces and a cgroup; the host kernel remains shared. Namespaces isolate views, while cgroups account and limit consumption. Here, memory.max=536870912 can lead to cgroup OOM after failed reclaim, cpu.max=50000 100000 throttles fair-class work after 50 ms per 100 ms, and pids.max=128 rejects a violating fork or clone with EAGAIN.

I would add UID mapping where appropriate, minimal capabilities, no_new_privs, seccomp, an AppArmor or SELinux policy, safe mounts, and restricted devices. I would verify namespace identities, UID maps, mounts, interfaces, effective cgroup files, capabilities, and security state from both sides, then run bounded tests and require matching memory.events, cpu.stat, and pids.events evidence.”

Step-by-Step Deep Dive

Step 1: Start with the Host-Process Model

The runtime receives an OCI bundle containing a root filesystem and configuration. A representative setup sequence is:

  1. validate the bundle, executable, mounts, namespace choices, credentials, and resource settings;
  2. create or select the cgroup subtree and write controller values;
  3. create new namespaces or join configured existing namespaces;
  4. establish UID/GID mappings if a user namespace is used;
  5. make mount propagation safe, mount the rootfs and special filesystems, and switch the process root;
  6. create or move network devices and configure routes when a new network namespace is used;
  7. set credentials, capabilities, no_new_privs, seccomp, and LSM labels or profiles;
  8. attach the process to the cgroup and exec the configured application.

Exact ordering and helper processes vary by runtime. The invariant is observable kernel state: the application remains a host-scheduled process with namespace memberships, credentials, mounts, filters, and a cgroup path. If the runtime crashes after partial setup, cleanup must remove mounts, interfaces, namespace pins, and cgroups; the word “container” is not a kernel object that performs cleanup automatically.

Step 2: Assign One Responsibility to Each Namespace

The main namespaces are complementary:

NamespaceIsolated viewImportant boundary
PIDProcess ID number space and visibilityThe same task has an inner PID and a host PID; container PID 1 must reap children and handle signals correctly
MountMount points and propagationIt changes the mount table, not the underlying kernel or automatically the backing storage
NetworkInterfaces, routes, ports, sockets, and network stackConnectivity is reintroduced deliberately with a veth pair, bridge, routing, or another network driver
UTSHostname and NIS domain nameIt is identity presentation, not authentication
IPCSystem V IPC and POSIX message queuesFiles or sockets deliberately shared through mounts can still connect workloads
UserUID/GID mappings and namespace-scoped capabilitiesUID 0 inside can map to an unprivileged host UID
CgroupView of the cgroup hierarchyResource enforcement comes from controllers, not from the cgroup namespace view
TimeBoot and monotonic clock offsetsIt does not provide arbitrary independent wall-clock hardware

A namespace type omitted from the OCI namespace list is inherited from the runtime. --pid=host, host networking, or joining another container's namespace can be intentional, but the answer must call out the lost isolation rather than still describing a fully private container.

The filesystem boundary needs both a mount namespace and a rootfs. A private or slave propagation mode prevents container mount events from unexpectedly flowing to the host. Read-only mounts, masked paths, a minimal /dev, and explicit bind mounts narrow access. A writable bind mount of the host root or the container-engine socket creates a direct high-impact path regardless of the process's private hostname.

Step 3: Derive the Three Resource-Limit Outcomes

Memory. memory.max=536870912 is the main hard limit for the cgroup and its descendants. As usage approaches the limit, the kernel attempts reclaim. If usage reaches the limit and cannot be reduced, the cgroup enters OOM handling; in the default mode the OOM killer may select a task in that cgroup, and memory.oom.group=1 can request treatment as one indivisible workload. A brief reading above the limit can occur. Check memory.current, memory.peak, and the max, oom, oom_kill, and oom_group_kill fields in memory.events. Do not diagnose every SIGKILL as a cgroup OOM without those counters and kernel or runtime evidence.

CPU. cpu.max=50000 100000 means that fair-class tasks in the group may consume up to 50,000 microseconds during each 100,000-microsecond period: an average bandwidth of 0.5 CPU. Multiple threads can spend the quota concurrently and exhaust it earlier in the period. Runnable tasks are then throttled until quota becomes available; they are not terminated. Inspect usage_usec, nr_periods, nr_throttled, and throttled_usec in cpu.stat. A latency spike near a period boundary can therefore be quota throttling even when host CPU is otherwise available.

Tasks. pids.max=128 is a hierarchical hard limit on kernel tasks. Threads count. Once a new task would violate the policy, fork() or clone() fails with EAGAIN; existing tasks keep running. Inspect pids.current, pids.peak, and the max count in pids.events. Moving existing tasks or lowering the configured limit can temporarily produce pids.current > pids.max; creation is still blocked from violating the policy.

Parent cgroups also constrain children. A child cannot obtain CPU, memory, or task capacity that its ancestors do not permit. Conversely, these three settings do not automatically limit every resource: storage space, I/O, file descriptors, network bandwidth, devices, and kernel-global objects need their own controls and operating limits.

Step 4: Layer Security Controls Around the Shared Kernel

User namespaces let a process be UID 0 inside while mapping to a normal unprivileged UID outside. That reduces the effect of a namespace escape or a mistaken host-file access, but mappings and filesystem ownership must be designed together. Without a user namespace, root in the container is still host UID 0, even if its capabilities and accessible objects are restricted.

Capabilities split traditional root privilege. Start from the minimum set instead of granting all capabilities. Dropping a capability is useful only if the process cannot regain it through file capabilities, set-user-ID execution, or another path; a bounded set and no_new_privs make that intent auditable.

Seccomp filters system calls and can allow, deny, trap, kill, log, or notify based on the call and arguments. It reduces reachable kernel attack surface but does not understand application-level authorization. An LSM such as AppArmor or SELinux applies policy to operations on files, processes, sockets, and other objects. Read-only filesystems, masked /proc paths, and a minimal device set add independent constraints.

These are layers, not substitutes. Cgroups mainly address accounting and resource denial of service; they do not prevent one container from reading another's data. Namespaces mainly isolate views; they do not patch the shared kernel. For adversarial tenants, kernel-exploit risk or compliance may justify a VM, microVM, or sandboxed-kernel boundary.

Step 5: Verify Kernel State from Both Sides

From the host, first identify the container's init process and cgroup path. A generic inspection sketch is:

bash
pid=<host-pid-of-container-init>
cg=/sys/fs/cgroup/<container-cgroup>

readlink /proc/$pid/ns/{pid,mnt,net,uts,ipc,user,cgroup}
cat /proc/$pid/uid_map
cat /proc/$pid/gid_map
cat /proc/$pid/cgroup

cat "$cg/memory.current" "$cg/memory.max" "$cg/memory.events"
cat "$cg/cpu.max" "$cg/cpu.stat"
cat "$cg/pids.current" "$cg/pids.max" "$cg/pids.events"

grep -E '^(CapPrm|CapEff|CapBnd|NoNewPrivs|Seccomp):' /proc/$pid/status

Compare namespace symlink device/inode identities with the host and with another container. Different identities prove different namespace instances; they do not alone prove safe mounts, routes, or policy. Inspect the actual mount table and propagation, network links and routes, effective capability set, seccomp mode, LSM label, and device nodes.

Inside the container, record /proc/1/status, /proc/self/cgroup, mount, hostname, visible processes, interfaces, routes, UID/GID, and namespace links. Use nsenter from an authorized host only for diagnosis; entering namespaces is privileged access, not evidence that the boundary failed.

Finally, run bounded failure tests in a disposable environment. Allocate memory gradually and correlate failure with memory.events; run CPU work and correlate latency with nr_throttled; create threads or processes until the next creation returns EAGAIN and pids.events increments. Stop the tests before host-level pressure, and verify that sibling cgroups remain healthy.

Step 6: Turn Observations into Acceptance Criteria

A defensible acceptance record contains values, identities, and outcomes:

  • namespace IDs differ where isolation is required and match only where sharing is intentional;
  • UID 0 mapping, effective capabilities, NoNewPrivs, seccomp mode, and LSM label match the threat model;
  • mount propagation, bind mounts, masked paths, writable paths, and device access match the OCI configuration;
  • memory.max, cpu.max, and pids.max equal 536870912, 50000 100000, and 128 at the effective cgroup;
  • memory pressure changes the relevant memory event counters, CPU load increases throttling counters without killing tasks, and the 129th task creation is rejected when 128 tasks are already charged;
  • parent and sibling cgroups remain within their own budgets during each test;
  • restart and forced-failure tests leave no unexpected mounts, interfaces, namespace pins, or populated cgroups.

The 129th-task statement is conditional on exactly 128 tasks already being charged to the effective hierarchy and no concurrent exits. In a real test, read pids.current immediately before creation rather than assuming an application has one task.

Strong Sample Answer

“I would begin with the host-process model. The runtime takes an OCI rootfs and config, creates or joins the requested PID, mount, network, UTS, IPC, user, cgroup, and time namespaces, configures mounts and networking, attaches the process tree to a cgroup, applies credentials and security policy, and execs the application. PID 1 inside is still a host process with another PID outside. The container has a private userspace view, while the host kernel remains shared.

Namespaces and cgroups solve different problems. The PID namespace changes process visibility; the mount namespace plus rootfs changes the visible filesystem; the network namespace provides its own interfaces, routes, ports, and sockets. User namespaces can map inner UID 0 to an unprivileged host UID. Cgroups account and control the process tree but do not hide host objects.

For the stated cgroup v2 values, memory.max=536870912 is a 512 MiB hard boundary. The kernel tries reclaim, then can invoke cgroup OOM handling if usage cannot be reduced; I would read memory.events to distinguish max, oom, and oom_kill. cpu.max=50000 100000 supplies at most 50 ms per 100 ms to fair-class work. Once quota is used, runnable tasks are throttled until more quota is available, and cpu.stat reports nr_throttled and throttled_usec; there is no CPU-limit kill. pids.max=128 counts kernel tasks, including threads. A violating fork or clone returns EAGAIN, and pids.events records the hit.

Because the kernel is shared, I would layer a user namespace where appropriate, a minimal capability set, no_new_privs, seccomp, an AppArmor or SELinux policy, read-only or masked mounts, and a minimal device set. I would reject privileged mode, host namespaces, broad host mounts, or engine-socket access unless each is an explicit trusted requirement. Hostile multi-tenancy may need a VM or microVM boundary.

To verify, I would compare /proc/{PID}/ns/* device/inode identities, UID/GID maps, mounts, interfaces, cgroup paths, effective controller files, capabilities, seccomp mode, and LSM labels from host and container. Then I would run bounded memory, CPU, and task tests and require the matching kernel counters and failure modes. A container that merely starts has not proved isolation.”

Common Mistakes and Improvements

  • Calling a container a small VM → this hides the shared kernel and produces wrong security assumptions → describe host processes with namespace, cgroup, filesystem, credential, and policy state.
  • Saying namespaces limit CPU and memory → namespaces isolate views, not consumption → map resource limits to cgroup controllers and files.
  • Saying cgroups isolate files and processes → cgroups group, account, and constrain tasks → map visibility to PID and mount namespaces.
  • Treating chroot as a container → changing the apparent root does not add PID, network, user, or resource isolation → combine a rootfs with mount and other required namespaces plus policy.
  • Claiming the CPU limit kills a process → CPU quota normally throttles runnable fair-class work → check cpu.stat for throttling.
  • Expecting memory usage to stop at exactly 512 MiB → reclaim, accounting timing, and temporary overage complicate the instant reading → use memory.max and event counters to establish the enforced outcome.
  • Counting only processes against pids.max the controller counts kernel tasks, so threads consume it → inspect pids.current before a limit test.
  • Assuming root inside is harmless → without a user namespace it may still be host UID 0, constrained only by other controls → inspect uid_map and capabilities.
  • Equating namespace separation with a secure tenant boundary → a kernel vulnerability or dangerous host mount can cross the process boundary → state the threat model and add policy or a VM boundary.
  • Checking configuration but not effective state → runtime flags can be overridden, inherited, or fail partially → read /proc and cgroup files, then exercise each limit.

Follow-Up Questions

Follow-Up 1: Why Does PID 1 Need Special Treatment Inside a Container?

The first process in a PID namespace is visible as PID 1 to its descendants. It adopts orphaned children and must reap them, or zombies accumulate and consume the pids budget. PID 1 also has special signal-handling semantics, so a wrapper that does not forward signals can make graceful termination fail. Verify the actual init process, child reaping, signal forwarding, and shutdown deadline rather than assuming the application framework handles them.

Follow-Up 2: What Is the Difference Between cpu.max and cpu.weight?

cpu.max sets a maximum bandwidth per period for fair-class work. It can throttle a cgroup even when the host has idle CPU after that group has consumed its current quota. cpu.weight is a proportional preference among runnable sibling cgroups during contention and does not by itself define a hard 0.5-CPU ceiling. A production policy may use both for different goals.

Follow-Up 3: Can Container Root Be Unprivileged on the Host?

Yes, when a user namespace maps UID 0 inside to an unprivileged UID range outside. Confirm the mapping in /proc/{PID}/uid_map and /proc/{PID}/gid_map. This narrows host privilege, but bind-mount ownership, subordinate-ID allocation, capabilities in the owning user namespace, and kernel attack surface still require review.

Follow-Up 4: Why Is a Private Mount Namespace Insufficient for Filesystem Security?

It isolates the mount table, but the runtime decides what backing filesystems and bind mounts appear there. A private namespace containing a writable bind mount of / still exposes the host root. Review mount sources, propagation, writable flags, masked and read-only paths, device nodes, and the process's credentials and LSM policy together.

Follow-Up 5: When Should You Prefer a VM or MicroVM?

Prefer a stronger boundary when mutually untrusted tenants execute arbitrary code, a shared-kernel escape is outside the accepted risk, compliance requires separate kernels, or kernel versions and modules must differ. The cost is additional startup, memory, image, and operations overhead. The decision follows the threat model and measured platform constraints, not the container or VM label alone.

Follow-Up 6: How Would You Diagnose a Container That Is Slow but Not OOM-Killed?

Correlate application latency with cpu.stat throttling, memory.events high/max pressure, pressure-stall information, I/O controller statistics, host scheduling, and network errors. A rising nr_throttled or throttled_usec with stable OOM counters points toward CPU quota. Reclaim or I/O pressure can also stall work without a kill, so the diagnosis needs aligned timestamps and effective cgroup ancestry.

Public sources

Related questions