Skip to content

Releases: crmne/mxc

Release list

v0.1.0

Choose a tag to compare

@crmne crmne released this 14 Oct 12:28
v0.1.0
1b4326a

MXC for Ruby 0.1.0 is the first release: run untrusted commands from Ruby in an operating system sandbox that decides what they can read, write, and reach, and how long they get.

gem install mxc

Precompiled gems are available for Linux (x86_64 and ARM64, glibc and musl), macOS (Intel and Apple Silicon), and Windows (x64 and ARM64), for Ruby 3.2 through 4.0.

New

Run a command in a sandbox with one call

result = MXC.run("python3 build.py", readable: "app/skills", writable: "tmp/out", timeout: 30)

result.success? # => true
result.stdout   # => "Built report.pdf\n"

MXC::Result also has stderr, exit_code, and timed_out?. By @crmne

Pass untrusted input as separate arguments

One String runs through the shell, like Kernel#system. A program and its arguments start directly, so each argument reaches the program unchanged:

MXC.run("python3", "convert.py", params[:filename], readable: "app/skills")

By @crmne

Describe policies the Ruby way

Declare a sandbox with class macros:

class SkillSandbox < MXC::Sandbox
  readable "app/skills"
  writable "tmp/skills"
  denied "app/skills/secrets"
  directory "app/skills"
  network allow: ["pypi.org"]
  timeout 30
end

SkillSandbox.new.run("python3", "build.py")

Or chain setters on MXC.sandbox: with_readable, with_writable, with_denied, with_network, with_env, with_directory, with_timeout, and with_backend. Paths take Strings or Pathnames, and timeouts take anything with to_f, including ActiveSupport::Duration. By @crmne

No network by default, or only the hosts you list

sandbox = MXC.sandbox.with_network(allow: ["pypi.org", "*.pythonhosted.org"])
result = sandbox.run("pip index versions flask")

result.blocked # => ["telemetry.example.com:443"]

The command reaches the listed hosts over HTTP and HTTPS and nothing else. By @crmne

Stream output and drive interactive processes

SkillSandbox.new.run("python3", "build.py") { |chunk| print chunk.content }

process = SkillSandbox.new.spawn("python3", "-i", "-q")
process.write("print(6 * 7)\n")
process.close_write
process.each { |chunk| print chunk.content }
process.wait.success? # => true

A process can also be stopped with kill. By @crmne

Know whether the machine can sandbox before you rely on it

MXC.supported? # => true

SkillSandbox.new.supported? # => false
SkillSandbox.new.availability.remediation
# => "Install slirp4netns, iptables and util-linux ..."

supported? starts a real sandbox once. MXC.backends lists what MXC found, and MXC.supports?(:network_policy) answers capability questions. By @crmne

Sandbox on Linux, macOS, and Windows

:auto picks Bubblewrap on Linux, Seatbelt on macOS, and Windows' own process containers (BaseContainer or AppContainer) on Windows. On Windows, one String runs through cmd.exe, and a program with separate arguments starts directly with each argument quoted. By @crmne

Run in Docker and Kamal

A guide with a minimal seccomp profile, the trade-offs of each /proc setup, a Bubblewrap wrapper that stops sandboxed code from creating user namespaces, and a Kamal job role configuration. By @crmne

Known limitations

  • No host allow-lists on Windows. MXC can only confine a Windows sandbox to a proxy by also accepting inbound connections to it, so with_network(allow: [...]) raises MXC::UnsupportedError there. network: false and network: true work.
  • Windows programs that need the UI subsystem cannot start in a sandbox, such as PowerShell and tasklist. Command-line tools such as Python and the cmd.exe built-ins work.
  • Windows builds without BaseContainer cannot hide denied paths. MXC's AppContainer fallback does not enforce them, so a sandbox with denied paths raises MXC::UnsupportedError there. This includes Windows Server, Windows 11 24H2, and the 25H2 builds that lack BaseContainer.
  • On Windows builds without BaseContainer, granted paths get temporary access control entries. MXC removes them when the command finishes, but a crash of your process during a run leaves them in place.
  • macOS gives sandboxed commands no writable /tmp. Give the command a writable directory and point TMPDIR at it.
  • Host allow-lists only cover programs that honor HTTP_PROXY and HTTPS_PROXY. Everything else, including raw sockets, UDP, and DNS, fails inside the sandbox. See Policies for what allow-lists do and do not protect against.
  • Network access on Linux needs extra packages: slirp4netns, iptables, util-linux, and /dev/net/tun. Sandboxes without network access need only Bubblewrap.
  • Docker needs a relaxed seccomp profile. Running in Docker explains exactly what it opens.

Thanks

Thanks to Microsoft's MXC team for building and open-sourcing MXC, and for the mxc-sdk crate this gem is built on. This gem is an unofficial binding and is not affiliated with or endorsed by Microsoft.

Read the documentation at crmne.github.io/mxc.

Full changelog: https://github.com/crmne/mxc/commits/v0.1.0