moonjail

    Linux least-privilege sandbox planning and runtime for MoonBit

    linux
    sandbox
    seccomp
    landlock
    security
    Download zip
    Author
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    11 hours ago
    Downloads
    2

    Dependencies

    #MoonJail

    MoonJail compiles a MoonBit policy into a Linux seccomp filter, Landlock file rules, and resource limits, then runs a child process under those limits. The policy compiler and explanation work across MoonBit targets; process execution requires Linux.

    The v0.1 prototype runs on Linux x86_64 and has been exercised on a host with Landlock ABI 8. Native execution on aarch64 and the built-in presets there still need hardware validation before a release. The latest local verification used MoonBit 0.1.20260920, Ubuntu x86_64 kernel 7.0.0-31-generic, and GCC 15.2.0; portable checks also passed on Windows with MoonBit 0.1.20260915.

    #Try the three-minute demo

    Install the current MoonBit toolchain and a C compiler. The demo needs only the project binary and standard Linux cat/head utilities; it does not require Python or a network service. From the repository root on Linux:

    moon run cmd/moonjail -- capabilities moon run cmd/moonjail -- explain moon run cmd/moonjail -- demo moon run cmd/moonjail -- check examples/deny-socket.json moon run cmd/moonjail -- explain-profile examples/deny-socket.json bash examples/agent-tool-smoke.sh

    demo shows a network socket rejected by seccomp, a declared file read, an undeclared /etc/passwd read rejected by Landlock, and the same read succeeding after its policy grants access. It prints structured JSON statuses. The successful retry reads zero bytes so it does not display the file contents. The demo uses MoonJail's own probe-socket subcommand as the test child. To test another command, use run-deny-socket <command> [args...]. The agent-tool scenario hashes an allowed fixture, rejects a disallowed file, and verifies that a file descriptor opened by the parent cannot bypass the Landlock policy. Its editable policy is examples/read-only-tool.json.

    #MoonBit API

    ///|
    test "compile a console tool policy" {
    let policy = @moonjail.Policy::from_profile(ConsoleTool)
    .allow_read("./workspace")
    .allow_write("./workspace/out")
    .limit_cpu(2)
    .limit_open_files(32)
    let plan = @moonjail.compile(policy, X86_64)
    assert_true(plan.seccomp.instructions.length() > 0)
    }

    The runtime API is @runtime.run(plan, command, args=[...], timeout_ms=30000). It reports Exited(code), Signaled(signal), TimedOut, or SetupFailed(stage, errno) and can serialize a result to JSON. Policy::to_json and Policy::from_json provide a versioned policy document; compile checks it before execution. The architecture guide describes the schema and process boundary.

    #Build and test

    moon fmt --check moon check --target all --warn-list +unnecessary_annotation moon test --target native moon build --target native

    Linux integration tests use Bash and standard coreutils to exercise seccomp and Landlock. On Windows, runtime tests confirm the unsupported-platform result while the portable compiler tests still run.

    The CLI returns 0 on success, 2 for usage errors, 3 for invalid input or runtime setup errors, and 10 when a sandboxed command exits unsuccessfully, is signaled, times out, or cannot be executed. Diagnostics for input/setup errors go to stderr; run still prints its structured result to stdout.

    The one-page application summary and development record are prepared for review.

    MoonJail limits a non-root child; it is not a container runtime. The caller must control inherited environment variables and standard streams. All non-standard descriptors are marked close-on-exec before the child runs; this requires Linux close_range with CLOSE_RANGE_CLOEXEC (kernel 5.11+), or execution fails closed. See SECURITY.md for the threat model and CONTRIBUTING.md for development guidance.

    The Mooncakes survey, GitHub survey, and hackathon proposal document the project choice.

    Licensed under Apache-2.0.

    CompileError

    pub(all) suberror CompileError {
    InvalidPolicy(Array[Diagnostic])
    UnknownSyscall(architecture~ : Architecture, name~ : String)
    InvalidProgram(String)
    } derive(
    Debug
    )

    ProfileError

    pub(all) suberror ProfileError {
    InvalidJson(String)
    UnsupportedVersion(Int)
    InvalidAction(String)
    InvalidRight(String)
    } derive(
    Debug
    )

    AccessRight

    pub(all) enum AccessRight {
    ReadFile
    ReadDir
    WriteFile
    Remove
    MakeNode
    Execute
    Refer
    } derive(Eq,
    Debug
    )

    Filesystem access rights enforced through Landlock on supported kernels.

    AccessRight::equal

    fn AccessRight::equal(AccessRight, AccessRight) -> Bool

    AccessRight::not_equal

    fn AccessRight::not_equal(x : AccessRight, y : AccessRight) -> Bool

    Architecture

    pub(all) enum Architecture {
    X86_64
    AArch64
    } derive(Eq,
    Debug
    )

    CPU architecture understood by the seccomp compiler.

    Architecture::audit_value

    fn Architecture::audit_value(self : Architecture) -> UInt

    Architecture::equal

    Architecture::not_equal

    fn Architecture::not_equal(x : Architecture, y : Architecture) -> Bool

    BpfInstruction

    pub(all) struct BpfInstruction {
    code : Int
    jt : Int
    jf : Int
    k : UInt
    } derive(Eq,
    Debug
    )

    One classic-BPF instruction as consumed by Linux seccomp.

    BpfInstruction::equal

    BpfInstruction::not_equal

    fn BpfInstruction::not_equal(x : BpfInstruction, y : BpfInstruction) -> Bool

    Diagnostic

    pub(all) struct Diagnostic {
    code : String
    message : String
    } derive(Eq,
    Debug
    )

    Policy or compiler diagnostics carry a stable code for tools and a human readable message for CLI users.

    Diagnostic::equal

    fn Diagnostic::equal(Diagnostic, Diagnostic) -> Bool

    Diagnostic::not_equal

    fn Diagnostic::not_equal(x : Diagnostic, y : Diagnostic) -> Bool

    PathRule

    pub(all) struct PathRule {
    path : String
    rights : Array[AccessRight]
    } derive(Eq,
    Debug
    )

    A path and the rights granted below it.

    PathRule::equal

    fn PathRule::equal(PathRule, PathRule) -> Bool

    PathRule::not_equal

    fn PathRule::not_equal(x : PathRule, y : PathRule) -> Bool

    PathRule::to_repr

    Policy

    pub(all) struct Policy {
    name : String
    default_action : SeccompAction
    syscall_rules : Array[SyscallRule]
    path_rules : Array[PathRule]
    limits : ResourceLimits
    require_landlock : Bool
    } derive(Eq,
    Debug
    )

    A complete, declarative sandbox policy.

    Policy::allow_by_default

    fn Policy::allow_by_default(name? : String) -> Policy

    Start a permissive policy for audit and migration work.

    Policy::allow_path

    fn Policy::allow_path(self : Policy, path : String, rights : Array[AccessRight]) -> Policy

    Policy::allow_read

    fn Policy::allow_read(self : Policy, path : String) -> Policy

    Policy::allow_syscall

    fn Policy::allow_syscall(self : Policy, name : String) -> Policy

    Policy::allow_write

    fn Policy::allow_write(self : Policy, path : String) -> Policy

    Policy::deny_by_default

    fn Policy::deny_by_default(name? : String) -> Policy

    Start a deny-by-default policy. Unknown syscalls fail with EPERM.

    Policy::deny_syscall

    fn Policy::deny_syscall(self : Policy, name : String, errno? : Int) -> Policy

    Policy::equal

    fn Policy::equal(Policy, Policy) -> Bool

    Policy::from_json

    fn Policy::from_json(text : String) -> Policy raise ProfileError

    Parse the version 1 JSON policy schema. The result is still validated and compiled before it can be used by the runtime.

    Policy::from_profile

    fn Policy::from_profile(profile : Profile) -> Policy

    Create one of the maintained baseline profiles.

    Policy::limit_cpu

    fn Policy::limit_cpu(self : Policy, seconds : Int) -> Policy

    Policy::limit_file_size

    fn Policy::limit_file_size(self : Policy, bytes : Int64) -> Policy

    Policy::limit_memory

    fn Policy::limit_memory(self : Policy, bytes : Int64) -> Policy

    Policy::limit_open_files

    fn Policy::limit_open_files(self : Policy, count : Int) -> Policy

    Policy::limit_processes

    fn Policy::limit_processes(self : Policy, count : Int) -> Policy

    Policy::not_equal

    fn Policy::not_equal(x : Policy, y : Policy) -> Bool

    Policy::on_syscall

    fn Policy::on_syscall(self : Policy, name : String, action : SeccompAction) -> Policy

    Add or override no rules: duplicate syscall names are rejected at compile time so the resulting policy is unambiguous.

    Policy::summary

    fn Policy::summary(self : Policy) -> String

    Summarize a policy before compilation.

    Policy::to_json

    fn Policy::to_json(self : Policy) -> String

    Serialize a policy using the stable version 1 JSON document schema.

    Policy::to_repr

    Policy::with_landlock_required

    fn Policy::with_landlock_required(self : Policy, required : Bool) -> Policy

    Profile

    pub(all) enum Profile {
    Minimal
    ConsoleTool
    BuildStep
    } derive(Eq,
    Debug
    )

    Built-in starting points. Profiles remain ordinary policies and can be extended with the builder methods.

    Profile::equal

    fn Profile::equal(Profile, Profile) -> Bool

    Profile::not_equal

    fn Profile::not_equal(x : Profile, y : Profile) -> Bool

    Profile::to_repr

    ResourceLimits

    pub(all) struct ResourceLimits {
    cpu_seconds : Int?
    address_space_bytes : Int64?
    file_size_bytes : Int64?
    open_files : Int?
    processes : Int?
    } derive(Eq,
    Debug
    )

    Resource limits applied before exec.

    ResourceLimits::equal

    ResourceLimits::not_equal

    fn ResourceLimits::not_equal(x : ResourceLimits, y : ResourceLimits) -> Bool

    SandboxPlan

    pub(all) struct SandboxPlan {
    name : String
    architecture : Architecture
    default_action : SeccompAction
    syscall_rules : Array[SyscallRule]
    seccomp : SeccompProgram
    path_rules : Array[PathRule]
    limits : ResourceLimits
    require_landlock : Bool
    } derive(Eq,
    Debug
    )

    Pure-data output of policy compilation. It can be inspected or serialized without invoking any platform API.

    SandboxPlan::equal

    fn SandboxPlan::equal(SandboxPlan, SandboxPlan) -> Bool

    SandboxPlan::explain

    fn SandboxPlan::explain(self : SandboxPlan) -> String

    Render a deterministic, human-readable explanation of a compiled plan.

    SandboxPlan::not_equal

    fn SandboxPlan::not_equal(x : SandboxPlan, y : SandboxPlan) -> Bool

    SeccompAction

    pub(all) enum SeccompAction {
    Allow
    Errno(Int)
    Trap
    Log
    KillProcess
    } derive(Eq,
    Debug
    )

    Result returned by a matching seccomp rule.

    SeccompAction::equal

    SeccompAction::not_equal

    fn SeccompAction::not_equal(x : SeccompAction, y : SeccompAction) -> Bool

    SeccompProgram

    pub(all) struct SeccompProgram {
    architecture : Architecture
    instructions : Array[BpfInstruction]
    } derive(Eq,
    Debug
    )

    A verified seccomp program plus the architecture it targets.

    SeccompProgram::disassemble

    fn SeccompProgram::disassemble(self : SeccompProgram) -> String

    SeccompProgram::equal

    SeccompProgram::not_equal

    fn SeccompProgram::not_equal(x : SeccompProgram, y : SeccompProgram) -> Bool

    SeccompProgram::to_words

    fn SeccompProgram::to_words(self : SeccompProgram) -> FixedArray[UInt]

    Pack instructions as [code, jt, jf, k, ...] for the native runtime shim.

    SyscallRule

    pub(all) struct SyscallRule {
    name : String
    action : SeccompAction
    } derive(Eq,
    Debug
    )

    One named syscall rule. The architecture-specific number is resolved when the policy is compiled.

    SyscallRule::equal

    fn SyscallRule::equal(SyscallRule, SyscallRule) -> Bool

    SyscallRule::not_equal

    fn SyscallRule::not_equal(x : SyscallRule, y : SyscallRule) -> Bool

    compile

    fn compile(policy : Policy, architecture : Architecture) -> SandboxPlan raise CompileError

    Compile an unambiguous policy into a linear classic-BPF seccomp program.

    syscall_number

    fn syscall_number(architecture : Architecture, name : String) -> Int?

    validate_policy

    fn validate_policy(policy : Policy) -> Array[Diagnostic]

    Validate policy-level invariants before architecture-specific compilation.

    verify_program

    fn verify_program(program : SeccompProgram) -> Unit raise CompileError

    Verify bounds and termination properties needed by the generated program.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io