{"id":51120925,"url":"https://github.com/discourse/ruby-landlock","last_synced_at":"2026-06-25T02:01:31.827Z","repository":{"id":354801475,"uuid":"1225091455","full_name":"discourse/ruby-landlock","owner":"discourse","description":"Landlock bindings for Ruby","archived":false,"fork":false,"pushed_at":"2026-04-30T06:43:13.000Z","size":62,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-13T08:04:03.689Z","etag":null,"topics":["landlock","linux","sandbox","security"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/discourse.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-30T00:04:02.000Z","updated_at":"2026-05-09T07:34:17.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/discourse/ruby-landlock","commit_stats":null,"previous_names":["discourse/ruby-landlock"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/discourse/ruby-landlock","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/discourse%2Fruby-landlock","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/discourse%2Fruby-landlock/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/discourse%2Fruby-landlock/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/discourse%2Fruby-landlock/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/discourse","download_url":"https://codeload.github.com/discourse/ruby-landlock/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/discourse%2Fruby-landlock/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34756206,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-25T02:00:05.521Z","response_time":101,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["landlock","linux","sandbox","security"],"created_at":"2026-06-25T02:01:31.056Z","updated_at":"2026-06-25T02:01:31.805Z","avatar_url":"https://github.com/discourse.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# landlock\n\nRuby bindings for Linux [Landlock](https://docs.kernel.org/userspace-api/landlock.html): unprivileged, kernel-enforced sandboxing for the calling thread and its future descendants.\n\nThis gem includes a small native extension around the three Landlock syscalls and a Ruby API for safe subprocess execution.\n\n## Status\n\nExperimental. Filesystem support requires Landlock ABI v1+. TCP network rules require ABI v4+. Signal and abstract Unix-domain socket scopes require ABI v6+.\n\n```ruby\nrequire \"landlock\"\n\nputs Landlock.abi_version\nputs Landlock.supported?\n```\n\nSee [CHANGELOG.md](CHANGELOG.md) for release notes.\n\n## Safe subprocess execution\n\nPass commands as an argument array. `Landlock.exec` and `Landlock.spawn` do not invoke a shell implicitly; use an explicit shell in the array if that is really required. Both helpers require a non-empty Landlock policy, accept `env:`/`unsetenv_others:` for controlled environments, and use the packaged native helper when available so the long-lived child is not a forked Ruby process.\n\nAllow Ruby to execute and read its runtime, but only allow outbound TCP connections to port 443:\n\n```ruby\nstatus = Landlock.exec(\n  [RbConfig.ruby, \"script.rb\"],\n  read: [\"/usr\", \"/lib\", \"/lib64\", \"/etc/ssl\"],\n  execute: [\"/usr\", \"/lib\", \"/lib64\"],\n  connect_tcp: [443],\n  allow_all_known: true\n)\n\nabort \"failed\" unless status.success?\n```\n\nDeny all outbound TCP except the listed ports:\n\n```ruby\nLandlock.exec(\n  [\"curl\", \"https://example.com\"],\n  read: [\n    \"/usr\", \"/lib\", \"/lib64\",\n    \"/etc/ssl\", \"/etc/resolv.conf\", \"/etc/hosts\",\n    \"/etc/nsswitch.conf\", \"/etc/gai.conf\", \"/etc/host.conf\",\n    \"/run/systemd/resolve\", \"/var/lib/sss\"\n  ].select { |path| File.exist?(path) },\n  execute: [\"/usr\", \"/lib\", \"/lib64\"],\n  connect_tcp: [443],\n  allow_all_known: true\n)\n```\n\nTLS and name-resolution dependencies vary by distribution and NSS configuration; add any local CA, DNS, NSS, or resolver paths your system needs.\n\nAllow binding a local TCP port:\n\n```ruby\nLandlock.exec(\n  [RbConfig.ruby, \"server.rb\"],\n  read: [\"/usr\", \"/lib\", \"/lib64\", Dir.pwd],\n  execute: [\"/usr\", \"/lib\", \"/lib64\"],\n  bind_tcp: [9292],\n  allow_all_known: true\n)\n```\n\n## Capturing subprocess output\n\n`Landlock.capture` is the stdout/stderr-capturing sibling of `Landlock.exec`: it launches a child process, applies Landlock rules, resource limits, and the optional seccomp network-deny filter before the target command starts, then execs that command directly. When the packaged `landlock-safe-exec` helper is available, `exec`, `spawn`, and `capture` all spawn that small native helper with policy arguments so the parent does not need to fork a bloated Ruby process; they fall back to the Ruby fork path when the helper cannot be used or when an unusually large helper argv would exceed the platform `ARG_MAX`. Environment changes are applied by Ruby when spawning the helper rather than encoded in helper argv. Use `capture!` when unsuccessful exit statuses should raise.\n\n```ruby\nresult = Landlock.capture(\n  [\"ffprobe\", \"-v\", \"error\", \"-show_format\", \"-of\", \"json\", upload_path],\n  read: [upload_path, \"/usr\", \"/lib\", \"/lib64\", \"/etc\"].select { |path| File.exist?(path) },\n  execute: [\"/usr\", \"/lib\", \"/lib64\"].select { |path| File.exist?(path) },\n  env: { \"PATH\" =\u003e ENV.fetch(\"PATH\", \"\") },\n  unsetenv_others: true,\n  rlimits: {\n    cpu_seconds: 5,\n    memory_bytes: 512 * 1024 * 1024,\n    file_size_bytes: 0,\n    open_files: 64,\n    processes: 0\n  },\n  seccomp_deny_network: true,\n  max_output_bytes: 256 * 1024,\n  truncate_output: false\n)\n\nmetadata = JSON.parse(result.stdout) if result.success?\n```\n\n`Landlock.capture` takes the command as a single argv array, like `Landlock.exec`. It returns a `Landlock::CaptureResult` with `stdout`, `stderr`, `status`, `success?`, `timed_out?`, and `output_truncated?`, including for unsuccessful exit statuses. It also supports array destructuring:\n\n```ruby\nstdout, stderr, status = Landlock.capture(\n  [\"tool\", \"arg\"],\n  read: [\"/usr\", \"/lib\", \"/lib64\", \"/etc\"].select { |path| File.exist?(path) },\n  execute: [\"/usr\", \"/lib\", \"/lib64\"].select { |path| File.exist?(path) }\n)\n```\n\n`Landlock.capture!` has the same return shape for successful commands, but raises `Landlock::CommandError` for unsuccessful statuses. The error also exposes `stdout`, `stderr`, `status`, and `result`.\n\n`Landlock.capture` requires an actual restriction: provide Landlock rules, `seccomp_deny_network: true`, or `rlimits:`. This avoids accidentally running a command completely unsandboxed when a dynamically built policy is empty. It also requires Linux Landlock support and raises `Landlock::UnsupportedError` when unavailable; it does not fall back to running the command unsandboxed.\n\nPass `stdin:` when a tool should read from standard input instead of a file:\n\n```ruby\nstdout, stderr, status = Landlock.capture(\n  [\"tr\", \"a-z\", \"A-Z\"],\n  stdin: \"hello\",\n  rlimits: { open_files: 64 }\n)\n```\n\nCapture options:\n\n- `read:`, `write:`, `execute:` — filesystem allowlists. Explicit paths must exist; missing paths raise `ArgumentError` instead of being silently ignored.\n- `paths:` — exact path rules with explicit Landlock rights, e.g. `{ path:, rights: %i[read_file] }`.\n- `connect_tcp:` and `bind_tcp:` — allowed TCP ports. TCP access is unrestricted unless a network rule is provided.\n- `scope:` — Landlock ABI v6+ scopes such as `:signal` and `:abstract_unix_socket`.\n- `seccomp_deny_network:` — additionally deny common Linux network syscalls with seccomp. This is Linux-specific and intended as defense in depth.\n- `rlimits:` — resource limits. Supported keys are `:cpu_seconds`, `:memory_bytes`, `:file_size_bytes`, `:open_files`, and `:processes`. Values must be non-negative integers.\n- `timeout:` — wall-clock timeout in seconds. On timeout capture terminates the process group and returns/raises with `result.timed_out?` true.\n- `max_output_bytes:` — combined stdout+stderr byte limit. With `truncate_output: false`, exceeding the limit raises `Landlock::CommandError` with the partial output captured before termination. With `truncate_output: true`, output is truncated and `result.output_truncated?` is true.\n- `stdin:` — string or IO-like object to write to the child process stdin.\n- `chdir:` — working directory for the child.\n- `env:` — environment entries for the child.\n- `unsetenv_others:` — clear the parent environment before applying `env:`.\n- `success_status_codes:` and `failure_message:` — `capture!` failure handling options.\n- `allow_all_known:` — when filesystem rules are present, handle all Landlock filesystem rights known to the running ABI so unlisted filesystem access is denied.\n\n## Restrict current process\n\nThis is irreversible for the current thread and its future children. Use `Landlock.exec` or `Landlock.spawn` unless you really mean it.\n\n```ruby\nLandlock.restrict!(\n  read: [\"/usr\", \"/app\"],\n  write: [\"/tmp/my-output\"],\n  connect_tcp: [443],\n  scope: [:signal, :abstract_unix_socket],\n  allow_all_known: true\n)\n```\n\n`write:` grants the filesystem rights needed for practical writes under the listed paths, including directory traversal and reads (`read_file`/`read_dir`). If you need exact rights, use `paths:` with an explicit `rights:` list.\n\n## Lower-level path rules\n\n```ruby\nLandlock.restrict!(\n  paths: [\n    { path: \"/usr\", rights: %i[read_file read_dir execute] },\n    { path: \"/tmp/out\", rights: %i[read_file read_dir write_file truncate make_reg remove_file] }\n  ],\n  connect_tcp: [443]\n)\n```\n\n## Performance\n\nLandlock enforcement is done by the kernel after a ruleset is installed. In normal use the practical cost should be dominated by the one-time sandbox setup and by the work your process already performs, not by Ruby-side wrappers.\n\nThis repository includes a small benchmark suite that compares common workloads before and after applying a read-only Landlock policy:\n\n```sh\nbundle exec rake bench\n# or\nbundle exec ruby benchmark/landlock_overhead.rb\n```\n\nThe suite reports median timings for CPU-only work, file metadata reads, small file reads, directory scans, and the one-time ruleset setup cost. You can tune the run length with environment variables:\n\n```sh\nSAMPLES=15 ITERATIONS=100000 DIR_ITERATIONS=5000 bundle exec rake bench\n```\n\nSample output looks like:\n\n```text\nworkload           baseline     landlocked        delta    delta %\n--------------------------------------------------------------------\ncpu_loop           0.650 ms       0.648 ms    -0.002 ms     -0.31%\nfile_stat         42.100 ms      42.300 ms     0.200 ms      0.48%\nfile_read        120.500 ms     120.900 ms     0.400 ms      0.33%\ndir_scan          88.000 ms      88.200 ms     0.200 ms      0.23%\n\nSetup cost (create ruleset, add read rules, restrict current process):\n  median 0.080 ms (25 samples)\n```\n\nTreat small positive or negative deltas as noise and benchmark on the kernel, filesystem, and hardware you deploy on. The expected result is no practical steady-state overhead for typical application work, with a small one-time cost when installing the sandbox.\n\n## Caveats\n\nLandlock is not a complete container. It restricts selected kernel-mediated actions for the current thread and its future descendants, but it does not create namespaces, hide process IDs, virtualize the filesystem, or isolate the process from every kernel interface. For serious untrusted execution, combine Landlock with a controlled environment, resource limits, seccomp, and process isolation appropriate to your threat model.\n\n`Landlock.restrict!` only installs a Landlock ruleset. It does not close already-open file descriptors, impose resource limits, clean the environment, or kill subprocess trees. The subprocess helpers add practical hardening around this: `exec`/`spawn` add controlled environments and `close_others`, while `capture` also adds optional `rlimits:`, optional `seccomp_deny_network:`, output limits, timeout handling, and process-group termination. This is still not a VM/container boundary. By default, subprocess helpers close inherited file descriptors numbered 3 and higher before installing the sandbox; pass `close_others: false` only when the child intentionally needs inherited descriptors. Direct `landlock-safe-exec` use also closes inherited descriptors by default.\n\nWhen the native helper is used, sandbox policy details such as allowed paths, TCP ports, scopes, rights, and rlimits are passed as helper argv. They may be visible to same-user processes through tools such as `ps` or `/proc/\u003cpid\u003e/cmdline` until the helper execs the target command. Environment values passed with `env:` are not encoded in helper argv, but do not put secrets in policy path names or other policy arguments.\n\nIf `Landlock.exec`, `Landlock.spawn`, or `Landlock.capture` child setup fails before `exec`, the child prints a diagnostic and exits 127. `landlock-safe-exec` setup/argument failures exit 126. These codes can collide with commands that legitimately exit with the same status, so inspect stderr when debugging failures.\n\nPath rules follow the kernel's normal path resolution when the rule is installed. Because paths are opened without `O_NOFOLLOW`, a symlink rule applies to the symlink target's inode, not to the symlink path itself. Capture APIs validate explicit `read:`, `write:`, and `execute:` paths before launching so typos fail closed instead of silently weakening a policy.\n\nLandlock only restricts access rights included in a ruleset's handled set: omitted categories remain allowed. Use `allow_all_known: true` when you want unlisted filesystem actions denied. Landlock TCP rules do not cover UDP or pathname Unix-domain sockets; ABI v6+ scopes can restrict signals and abstract Unix-domain sockets. `seccomp_deny_network:` is Linux-specific defense in depth for common network syscalls, not a general-purpose seccomp policy language.\n\n`Landlock.restrict!` applies to the calling thread and its future children; already-running sibling threads are not retroactively sandboxed. Prefer `Landlock.capture`, `Landlock.exec`, or `Landlock.spawn` for subprocess sandboxing from a larger Ruby application.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiscourse%2Fruby-landlock","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdiscourse%2Fruby-landlock","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiscourse%2Fruby-landlock/lists"}