@@ -25,7 +25,7 @@ protects your working directory automatically.
2525| Network isolation | Landlock port range + seccomp notif port virtualization | Network namespace | TAP device | Sentry kernel |
2626| Syscall filtering | seccomp-bpf | seccomp | N/A (full kernel) | Sentry kernel |
2727| Resource limits | seccomp notif + SIGSTOP/SIGCONT | cgroup v2 | VM config | cgroup v2 |
28- | Memory sharing | COW ( fork), zero-copy | Bind-mount + re-init | Shared mem (explicit) | N/A |
28+ | Memory sharing | COW fork ( ` sb. fork() ` ), zero-copy | Bind-mount + re-init | Shared mem (explicit) | N/A |
2929| Nesting | Native (fork) | Complex (DinD/DooD) | Not supported | Supported |
3030| COW filesystem | OverlayFS (kernel) / BranchFS | Overlay | Block-level | N/A |
3131| Checkpoint/restore | ptrace + BranchFS | CRIU | VM snapshot | N/A |
@@ -140,6 +140,22 @@ with Sandbox(parent_policy) as parent:
140140 result = parent.sandbox(child_policy).run([" python3" , " untrusted.py" ])
141141```
142142
143+ COW fork — init once, fork many (for AI agents, RL rollouts):
144+
145+ ``` python
146+ def init ():
147+ global model
148+ model = load_model() # expensive, done once
149+
150+ def work ():
151+ seed = int (os.environ[" SEED" ])
152+ rollout(model, seed) # reads COW-shared model
153+
154+ with Sandbox(policy, init, work) as sb:
155+ for seed in range (1000 ):
156+ sb.fork(env = {" SEED" : str (seed)}).wait()
157+ ```
158+
143159## Architecture
144160
145161Sandlock applies confinement in sequence after ` fork() ` :
@@ -317,6 +333,40 @@ cp = Checkpoint.load("my-env")
317333Sandbox.from_checkpoint(cp, restore_fn = lambda state : load_state(state))
318334```
319335
336+ ### COW Fork
337+
338+ Initialize expensive state once, then fork COW clones that share memory.
339+ Each ` fork() ` is an ` os.fork() ` — the kernel shares all pages copy-on-write.
340+ 1000 clones of a 50 MB Python process use ~ 50 MB total, not 50 GB.
341+
342+ ``` python
343+ def init ():
344+ global model, data
345+ model = load_model() # 2 GB, loaded once
346+ data = preprocess_dataset() # stored in globals
347+
348+ def work ():
349+ seed = int (os.environ[" SEED" ])
350+ result = rollout(model, data, seed) # reads COW-shared globals
351+ save_result(result)
352+
353+ with Sandbox(policy, init, work) as sb:
354+ for seed in range (1000 ):
355+ sb.fork(env = {" SEED" : str (seed)}).wait()
356+ ```
357+
358+ How it works:
359+ 1 . ` Sandbox(policy, init, work) ` forks a child, runs ` init() ` , then the
360+ child's main thread enters a fork-ready loop on the control socket
361+ 2 . ` sb.fork(env=...) ` sends a command — the main thread calls ` os.fork() ` ,
362+ the clone applies env overrides and runs ` work() `
363+ 3 . No signals, no ptrace — the main thread is in a blocking ` os.read() `
364+ (GIL released), so it forks itself cleanly
365+ 4 . Each clone inherits Landlock + seccomp confinement via ` fork() `
366+
367+ Designed for AI agent sandboxing and RL rollouts where setup is expensive
368+ but each run needs different parameters.
369+
320370### Privileged Mode
321371
322372` privileged=True ` maps UID 0 inside a user namespace. The child appears as
0 commit comments