Running third-party extractors on user files is the kind of feature that looks safe until you read the CVE list for any single one of them, and LinSync’s plugin host does exactly that, all day, on documents and images that came from email attachments, cloud drives, and the web, which means the safety of the whole application depends on the safety of binaries we did not write and cannot fully audit.
This is why LinSync, the document and image compare tool we ship, runs every plugin inside a sandbox that layers Landlock, seccomp, and bubblewrap, and the interesting part is not that any one of these exists, it is that the three together form a containment strategy that is narrow enough to stay fast and broad enough to catch the cases any single layer misses.
The plugin host’s actual job
LinSync compares documents and images, and to do that it extracts text, metadata, and rendered pages from files that arrive in formats most people never open directly: PDF, DOCX, ODT, TIFF, PNG, and a long tail of archival formats. The extraction is handled by third-party tools, Tesseract for OCR, Poppler for PDF rendering, LibreOffice for office documents, ImageMagick for image conversion, and roughly two hundred smaller utilities, and each of these is a native binary with its own parser, its own dependencies, and its own history of memory-corruption bugs.
The plugin host’s job is to run those binaries as if they were untrusted, because from a security perspective they are, and the only thing a plugin should be allowed to do is read the input file, write the output file, and exit.
Landlock: the filesystem is the first wall
Landlock is the first layer because it is the cheapest to set up and the most precise in what it denies, but it is also a one-way ratchet: a process can only add restrictions, never remove them, which means the policy has to be built before the plugin is executed and the per-job model is load-bearing, because if you get the policy wrong the only fix is to kill the process and start over. The host builds a policy that whitelists exactly two directories, the input staging directory and the output staging directory, and allows only the file operations needed for a single job, so the plugin can read /tmp/linsync/in/<jobid>/document.pdf and write /tmp/linsync/out/<jobid>/result.json, but it cannot open /etc/passwd, cannot enumerate the home directory, cannot write to ~/.bashrc, and cannot create files anywhere outside the two whitelisted paths.
The policy is generated per job, which means every plugin invocation gets a fresh, minimal view of the filesystem, and the kernel enforces it directly without a userspace daemon or a container runtime. The catch, and it is a real catch, is that Landlock needs a Linux 5.13+ kernel for the first version of the ABI and 6.2+ for the second version that adds rename, unlink, and TCP bind/connect restrictions, plus the CONFIG_SECURITY_LANDLOCK flag, so on older kernels or some hardened distros the host falls back to the next layer.
seccomp: syscalls are the second wall
If Landlock is the filesystem guard, seccomp is the syscall guard, and the two overlap but neither replaces the other. The seccomp filter is a whitelist compiled from a policy file and loaded with seccomp_load, and it lists the exact syscalls a plugin is allowed to make: read, write, openat, close, mmap, munmap, and a handful of others needed for a well-behaved extractor, with argument checks where it matters.
Network syscalls are blocked outright, so socket, connect, sendto, and bind return EPERM or kill the process. execve is blocked, so a compromised plugin cannot spawn a child. ptrace is blocked. The default action for any syscall not on the whitelist is SECCOMP_RET_KILL_PROCESS, which means a plugin that tries to escape by calling something unusual is terminated immediately rather than merely denied.
The filter is written to be pessimistic: if a syscall is not obviously needed, it is not allowed, and when a new plugin breaks because it needs an obscure syscall, the failure is loud and explicit, which is preferable to the silent failure of an overly permissive policy.
bubblewrap: the namespace fallback
Landlock and seccomp are elegant, but they are also kernel-dependent, and a sandbox that only works on some machines is not a sandbox, it is a hope. bubblewrap is the fallback that uses mount namespaces, PID namespaces, and capability dropping to create a minimal container around the plugin, and it works wherever the binary is installed and the kernel has user namespaces enabled, which is most modern Linux distributions, though some hardened distros and enterprise configurations disable unprivileged user namespaces or limit them, and on those systems bubblewrap is often installed setuid so it can still create the namespace without the kernel flag.
The bubblewrap invocation mounts only the input and output directories into the container, hides the rest of the host filesystem, drops all capabilities, disables network access, and runs the plugin as an unprivileged user. It is heavier than Landlock and seccomp because it involves fork, mount, and namespace setup, but it is also the broadest layer, and it catches the cases where the first two are unavailable or misconfigured.
Why three layers instead of one
One layer is never enough because each layer has a failure mode: Landlock does not stop a plugin from calling socket, seccomp does not stop a plugin from reading a permitted file that turns out to be sensitive if the policy was too broad, and bubblewrap is heavier and more complex to maintain. The three layers are defense in depth, not redundancy, and the plugin only escapes if it defeats all three in the same invocation.
This is the same philosophy as a network firewall stack: you do not pick one good filter and call it done, you stack filters so that the failure of any one is contained by the others.
The cost
The cost is real: every plugin invocation pays the setup cost of loading a seccomp filter, applying a Landlock policy, and possibly spawning a bubblewrap namespace, and on the machines we test against the median overhead is around three milliseconds, with the tail pushed higher by bubblewrap jobs on heavily loaded boxes, but on a job queue with thousands of small documents it adds up to real time. The alternative is to run plugins unsandboxed, which is faster and also unacceptable, because the whole point of a compare tool is that it opens files you do not trust, and trusting the parser is the wrong place to optimize.
We measure that overhead in practice, not in theory, and the rule is simple: if a plugin can be sandboxed, it is sandboxed, and if a plugin cannot be sandboxed because it needs capabilities no sandbox can grant, it does not ship.
The tradeoff, stated plainly
What you give up is the simplicity of spawning a subprocess and trusting the binary, and what you gain is the ability to run arbitrary third-party extractors on arbitrary user files without trusting the authors of those extractors. The sandbox is not a guarantee, it is a containment strategy, and a good containment strategy is the difference between a bad plugin and a bad day.
The modern equivalent of this approach is the same as it was in the 2000s when we ran untrusted code in chroot jails: assume the binary is hostile, give it the smallest possible view of the world, and kill it the moment it tries to look outside. Landlock, seccomp, and bubblewrap are just the 2026 tools for a 2006 idea.
