Why build a new low-level container runtime?

The idea of separating the low-level container runtime concerns into its own tool or microservice is not new. Outside of the Kubernetes CRI, which presents container lifecycle management as a pluggable microservice, there are simpler tools which provide a low-level container runtime as well, such as the unshare utility in util-linux, as well as another tool called Bubblewrap.

But these tools are either too high-level (like the Kubernetes CRI), or they are designed to be used via shell scripting: Bubblewrap has a high amount of configurability, but is only accessible via a very complicated CLI that is easy to get wrong, while util-linux’s unshare has basic functionality, but also locked behind a CLI. While CLIs allow for rapid iteration, we needed something different for Edera Protect: a rich programmatic interface for spawning and managing containers with precision. Styrolite provides the best of both worlds—a clean API directly usable from Rust, while preserving the rapid iteration capabilities of a CLI.

How Styrolite Works: Linux Containers Under the Hood

Importantly, we designed Styrolite with full awareness that Linux namespaces were never intended as hard security boundaries—a fact that explains why container escape vulnerabilities continue to emerge. Our approach acknowledges these limitations while providing a more robust foundation.

Containers in Linux – as opposed to other containerization efforts elsewhere – are built on Linux namespaces. A namespace in Linux allows for an alternative view of a given type of resource, for example: the mount namespace allows for an alternative view of the mount table, and thus an alternative view of the root filesystem, while the PID namespace allows for an alternative view of the system process set. Container runtimes compose these namespaces to provide containerized environments, allowing for great flexibility at the cost of higher complexity.

At the root of all container runtimes is the Linux unshare(2) syscall.  This allows for a currently running process to dissociate from one or more of its current namespaces into a forked version of the namespace that can be modified.  In general, container runtimes unshare most namespaces by default because it is necessary to do:

  • Mount namespaces are unshared from the host so that we can pivot into a modifiable copy of a container image as the container’s root filesystem.
  • PID namespaces are unshared from the host so that the containerized workload cannot view processes outside of the container.
  • IPC namespaces are unshared from the host so that the containerized workload cannot view or manipulate System V IPC resources unrelated to the containerized workload.
  • User namespaces are unshared from the host so that UIDs and GIDs inside the container are remapped to safer UIDs and GIDs in the host namespace.  In other words, root inside a container when running in a user namespace is usually running as an unprivileged UID outside the container environment.
  • Time namespaces may be unshared from the host to modify the visible system uptime.
  • UTS namespaces may be unshared from the host to change the visible system hostname.
  • Network namespaces may be unshared from the host to force the containerized workload to use an alternate networking path.

On the Edera Protect platform, all of these namespaces are unshared at different levels in the stack.  Styrolite when used inside the Edera Protect platform is primarily responsible for dealing with the Mount, PID, IPC, User, Time and UTS namespaces, while networking is handled elsewhere in the platform.

Simple, Programmatic Interface

Here's a glimpse of how Styrolite simplifies container creation compared to traditional approaches:

// Create a basic container with Styrolite
let mut request = CreateRequestBuilder::new()
    .with_rootfs("/path/to/container/rootfs")
    .set_executable(“/bin/sh”)
    .set_arguments(vec![“-i”])
    .set_working_directory(“/”)
    .push_namespace(Namespace::User)
    .push_namespace(Namespace::Mount)
    .push_namespace(Namespace::Pid)
    .to_request();

// Launch a process in the container
let runner = Runner::new(“styrolite”);
runner.run(request)?;

This clean interface makes container creation and management more maintainable and less error-prone than complex CLI commands or shell scripts.

Real-World Applications

Styrolite powers several important use cases:

  • Secure microservices: Within Edera Protect, Styrolite enables fine-grained container isolation for security-critical workloads
  • Application sandboxing: Our companion tool, styrojail, helps Linux users limit resource consumption and improve security for applications like web browsers that process untrusted input
  • Custom CI/CD environments: Developers can use Styrolite to create isolated build environments with precise resource controls

Performance and Security

Styrolite is designed with minimal overhead, providing container initialization times comparable to or faster than traditional CLI approaches while offering stronger programmatic guarantees about container state.

Our security-first design acknowledges the inherent limitations of Linux namespaces while providing a more robust foundation through careful defaults and explicit security controls.

Join the Community

When we decided to build a low-level containerization tool for our own use in Edera Protect, we immediately knew it would have other benefits to the greater OSS community. We welcome contributions from the community:

  • Star and fork the project on GitHub
  • Try out Styrojail for application sandboxing
  • Report issues or suggest improvements
  • Contribute code or documentation

We're committed to Styrolite's continued development and look forward to seeing how the community builds with it!