skip to content

A clinical-lab result loader must run user-submitted transform scripts; how do you choose the isolation boundary and validate it against a 340-case regression pack?

level: principalimportance: should knowfreq 22%

answer

  1. Reject in-process before comparing options
  2. Boundary scales with threat and budget
  3. Process, then container, then VM
  4. Correctness pack is not a security control
  5. Add a separate adversarial escape suite

basics

~20 s

Decide the boundary first: never in-process. Run each submitted script in a short-lived separate process inside a locked-down container (dropped privileges, no network, read-only or disposable filesystem, CPU/memory/time limits), escalating to a microVM for stronger isolation. The 340-case regression pack proves correctness, not containment — pair it with an adversarial escape suite.

solid answer

~50 s

The first and non-negotiable decision is that in-process restriction is off the table: attribute traversal, `gc.get_objects()`, and frame reachability make it unsound, so the only question is *which* operating-system boundary. The floor is a short-lived child process (via `subprocess`) running as an unprivileged user with `resource` limits; a container adds filesystem and namespace isolation; a microVM is the strongest and the right default for arbitrary, externally-submitted code. Scope the decision by threat and latency budget: internal, reviewed transforms tolerate a lighter boundary than anonymous uploads. Critically, the 340-case regression pack validates *behaviour* — that transforms produce correct lab results — and says nothing about *security*; you must add an adversarial suite that asserts attempted `os`, network, and filesystem access is blocked, and enforce wall-clock and memory ceilings because untrusted code can hang or exhaust resources regardless of boundary. Log every run for audit, given the clinical context.

code

python · 10 lines
python
import subprocess, sys

# The boundary is a separate, disposable process with a hard timeout -
# never exec() in the loader's own interpreter.
untrusted = "print(sum(range(10)))"
proc = subprocess.run(
    [sys.executable, "-I", "-c", untrusted],
    capture_output=True, text=True, timeout=5,
)
print(proc.stdout.strip())   # 45

go deeper

for a junior

Recognise that untrusted transform scripts must run outside the loader's own process, and that a timeout and limited privileges are basic requirements. The details of choosing between container and VM are for more senior engineers.

for a middle

Be able to describe the process-plus-resource-limits floor and why in-process exec is rejected, and explain that correctness tests do not prove containment.

for a senior

Design the concrete boundary — disposable process, container, or VM — with dropped privileges, no network, resource ceilings, and least-privilege data access, and stand up a separate adversarial escape suite alongside the regression pack.

for a principal

Own the policy and the tradeoff: reject in-process outright, scale the boundary to submitter trust and latency budget, version runner-plus-boundary together, gate both correctness and containment in CI, and set a default-deny posture with kill switches and audit logging for the clinical context.

## Framing the decision This is a lead's call, and the trap is treating it as 'how do I lock down the interpreter'. The correct framing is 'what operating-system boundary do I put between the submitted transform scripts and the clinical-lab loader, and how do I prove both correctness and containment'. The result loader ingests lab results and applies user-submitted transform scripts to them — so the blast radius of a bad or malicious script includes patient data and the loader's own credentials. Getting the boundary right is the whole job. ## Step 1: rule out in-process Before comparing options, state plainly that in-process sandboxing is unsound and why: from any object, attribute traversal reaches `object.__subclasses__()` and function `__globals__`; `gc.get_objects()` surfaces the entire heap; frames expose the caller. No namespace restriction closes these redundant paths. Any proposal that keeps untrusted transforms inside the loader's own interpreter is rejected on principle, not risk-weighted. ## Step 2: choose the boundary by threat and budget The axis is *how much isolation* the threat model and latency budget justify: - **Separate process (floor).** Launch each transform as a child process with a distinct, unprivileged user, using `subprocess` with a hard `timeout`. Apply `resource` limits (CPU seconds, address-space/memory, file size, no core dumps). This alone means a breakout reaches only the child's own trivial objects, and the parent can kill it. It is the *minimum*, not the target for untrusted input. - **Container.** Wrap the process in a container to add filesystem, PID, and network namespaces: no network, a read-only or throwaway root, a non-root user, dropped capabilities. Right for semi-trusted or internal-but-arbitrary scripts. (The container flags themselves are a container-platform topic; the architectural point is that this is the layer that gives filesystem and network isolation.) - **MicroVM / VM.** For arbitrary, externally-submitted code — which anonymous 'user-submitted transform scripts' effectively is — a microVM or VM gives a kernel/hardware boundary that survives a container escape. This is the sensible default when submitters are untrusted. Match the boundary to the submitter: reviewed internal transforms can run behind the lighter boundary; anonymous uploads get the VM. Do not pick one boundary for all inputs if the trust levels differ. ## Step 3: resource and data controls independent of the boundary Whatever the boundary, untrusted code can hang, allocate without bound, or emit garbage. Enforce wall-clock timeouts (kill the process), memory ceilings, output-size caps, and CPU limits. Give the transform only the data it needs — a single result record or batch — never the loader's full connection or credential set. In a clinical context, log every submission, its hash, the user, the resource usage, and the outcome for audit and reproducibility. ## Step 4: what the 340-case regression pack does and does not prove This is the part that separates a principal answer from a senior one. The 340-case regression pack is a *correctness* instrument: it asserts that for 340 known inputs the transforms produce the expected lab-result outputs, catching functional regressions when you change the runner, the Python version, or the transform API. It proves nothing about *security*. A transform can pass all 340 correctness cases and still attempt to exfiltrate data or escape. So the regression pack must be paired with a separate **adversarial/escape suite**: cases that submit scripts attempting `import os`, network connections, filesystem writes outside the sandbox, `object.__subclasses__()` gadget hunts, `gc.get_objects()` scans, and resource exhaustion — each asserting the boundary blocks or contains the attempt. Wire both suites into CI so a boundary regression (say, a container losing its `no-network` setting) fails the build the same way a numeric regression does. Track them separately: 'correctness 340/340' and 'containment N/N' are different guarantees. ## Step 5: operational posture Default-deny: a new transform runs behind the strongest boundary until reviewed. Version the runner and the boundary config together so the regression and escape suites test the exact deployed combination. Have a kill switch and rate limits so a flood of expensive submissions cannot starve the loader. Revisit the boundary choice when the submitter population or data sensitivity changes. ## The judgement being tested The interviewer wants to hear: in-process is rejected outright; the boundary is chosen by threat and latency, not by convenience; resource limits and least-privilege data access are enforced regardless of boundary; and — the discriminator — that a correctness regression pack is not a security control, so containment is validated by a separate adversarial suite that is part of the same gate.

  • The team proposes running each transform in its own 3.14 subinterpreter to avoid process overhead. Do you accept it?
    Not as the security boundary. Subinterpreters (concurrent.interpreters) isolate module state and can hold their own GIL, which helps state hygiene and parallelism, but they share one OS process and address space — a breakout or C-level crash affects the whole loader, and resource exhaustion is not contained. Use them for performance isolation of trusted code; keep the untrusted transforms behind a process/container/VM boundary.
  • How do you keep the escape suite honest as the runner evolves?
    Version the runner and the boundary configuration together and run both suites against the exact deployed combination in CI, so a config drift (a lost no-network flag, a relaxed capability) fails the build. Treat any new escape technique that surfaces as a new regression case added permanently to the suite, and require the strongest boundary by default until a transform source is reviewed.
  • Anonymous external users submit transforms versus a handful of reviewed internal analysts — does the boundary change?
    Yes. Match the boundary to the submitter's trust: reviewed internal analysts can run behind a locked-down container, while anonymous external submissions warrant a microVM or VM for a kernel/hardware boundary that survives a container escape. Do not apply one boundary uniformly when trust levels differ; scope it per source and default-deny new sources to the strongest tier.

saying these in an interview costs you the question

  • Just empty __builtins__ and run transforms in-process
  • Passing the 340 regression cases proves it is secure
  • Subinterpreters isolate untrusted code well enough
  • One boundary fits every submitter regardless of trust
  • No need for timeouts if the code is short
  • Give the transform the loader's full DB connection

context