chore: adopt node-addon-landlock-run source as native/ subtree
Bring the node-addon-landlock-run tree (tag v0.0.1, commit 614f7fd) into native/landlock-run as its source of record: launcher development happens here, next to the harness consumers, and the standalone repository becomes the release mirror the tree is exported to for packing and publishing (procedure in native/README.md). The subtree keeps its own pnpm workspace and lockfile and is NOT added to the harness workspace: harness installs, gates, and CI never touch it. The mirror's .github/ stays out of the subtree; a separate manually-dispatched workflow (.github/workflows/landlock-run.yml) runs the subtree's CI legs — the per-architecture native builds, real-kernel launcher proofs, and pack rehearsal — adapted with working-directory/cache paths. eslint ignores the subtree like vendor/; AGENTS.md gains the native/ layout line (+5 words on its budget ceiling).
This commit is contained in:
16
native/landlock-run/packages/entry/README.md
Normal file
16
native/landlock-run/packages/entry/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# node-addon-landlock-run
|
||||
|
||||
Landlock self-restrict-then-exec launcher for confining subprocesses on Linux: this entry package resolves the per-platform prebuilt binary, runs its functional enforcement probe, and builds its grant argv — consumers never spell launcher flags or parse launcher output themselves.
|
||||
|
||||
```js
|
||||
import { grantArgs, launcherPath, probe } from 'node-addon-landlock-run';
|
||||
|
||||
const launcher = launcherPath();
|
||||
if (probe(launcher) !== 'unusable') {
|
||||
const argv = [launcher, ...grantArgs({ readOnly: ['/'], readWrite: ['/tmp/work'] }), '--', 'bash', '-c', command];
|
||||
}
|
||||
```
|
||||
|
||||
The launcher installs a Landlock ruleset on itself and `exec`s the wrapped command; the ruleset is inherited across `execve`, so the whole process tree runs confined. Everything not granted is denied, and launcher failures exit `125` without running the command — fail-closed, never fail-open. The binary contract is pinned in the repo's `docs/cli-contract.md`; the C source rides this tarball (`src/main.c`) for audit.
|
||||
|
||||
Platform packages (`os`/`cpu`-selected optional dependencies, no JavaScript inside): `node-addon-landlock-run-linux-x64`, `node-addon-landlock-run-linux-arm64`. On hosts without one, `launcherPath()` returns a deterministic nonexistent path and `probe()` reports `'unusable'` — there is deliberately no install-time compile fallback.
|
||||
36
native/landlock-run/packages/entry/package.json
Normal file
36
native/landlock-run/packages/entry/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "node-addon-landlock-run",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"description": "Landlock self-restrict-then-exec launcher for sandboxing subprocesses on Linux: per-platform prebuilt static binaries plus the JS seam that resolves, probes, and speaks their CLI contract",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"lib/",
|
||||
"!lib/*.tsbuildinfo",
|
||||
"src/main.c"
|
||||
],
|
||||
"scripts": {
|
||||
"build:js": "tsc -b",
|
||||
"prepack": "node ../../scripts/verify-entry-lib.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-addon-landlock-run-linux-arm64": "workspace:*",
|
||||
"node-addon-landlock-run-linux-x64": "workspace:*"
|
||||
}
|
||||
}
|
||||
126
native/landlock-run/packages/entry/src/index.ts
Normal file
126
native/landlock-run/packages/entry/src/index.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* The JS seam over the prebuilt `landlock-run` launcher: resolve the
|
||||
* binary for this host, build its grant argv, and run its functional probe.
|
||||
*
|
||||
* This module owns the launcher's CLI contract (`docs/cli-contract.md`) so
|
||||
* consumers never parse launcher output or spell launcher flags themselves —
|
||||
* the contract and the binaries version together in one package family,
|
||||
* which makes probe-parsing drift against the binary structurally
|
||||
* impossible. Policy stays with the consumer: this package does not know
|
||||
* what a "sandbox mode" is, only which paths are granted read or write.
|
||||
*
|
||||
* Deliberately no environment-variable overrides anywhere in this module:
|
||||
* which binary confines a process must never be decidable by the ambient
|
||||
* environment. Test injection is by function parameter.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { createRequire } from 'node:module'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
/** The launcher binary's file name inside each platform package's `bin/`. */
|
||||
export const LAUNCHER_BIN = 'landlock-run'
|
||||
|
||||
/**
|
||||
* The exit code for every launcher-level failure (usage error, unenforcing
|
||||
* kernel, unopenable grant root, failed exec) — chosen because the wrapped
|
||||
* command itself is unlikely to use it, so a consumer can tell launcher
|
||||
* failures from command failures. Part of the CLI contract.
|
||||
*/
|
||||
export const LAUNCHER_FAILURE_EXIT = 125
|
||||
|
||||
/**
|
||||
* The probe's verdict on this host: `full` when the running kernel enforces
|
||||
* every access the launcher can govern, `partial` when an older Landlock ABI
|
||||
* governs only a subset (still confined for everything it supports), and
|
||||
* `unusable` when nothing can be enforced — a kernel without Landlock, a
|
||||
* disabled LSM, or a missing binary, all indistinguishable on purpose
|
||||
* because the consumer's answer is the same: do not trust this launcher.
|
||||
*/
|
||||
export type LandlockEnforcement = 'full' | 'partial' | 'unusable'
|
||||
|
||||
/**
|
||||
* Filesystem grants for one confined run. Everything not granted is denied —
|
||||
* Landlock rulesets are allow-lists.
|
||||
*/
|
||||
export interface LauncherGrants {
|
||||
/** Roots granted read + execute beneath (the launcher's `--ro`). */
|
||||
readonly readOnly?: readonly string[]
|
||||
/** Roots granted full filesystem access beneath (the launcher's `--rw`). */
|
||||
readonly readWrite?: readonly string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Path of the launcher binary for this host: resolved from the per-platform
|
||||
* npm package `node-addon-landlock-run-<platform>-<arch>` (npm's
|
||||
* `os`/`cpu` fields make installers fetch only the matching one). When the
|
||||
* package is not resolvable — a platform without one, or an install that
|
||||
* skipped the optional dependency — the returned fallback path points inside
|
||||
* this package's own `node_modules` and simply never exists. Existence is
|
||||
* deliberately not checked either way: {@link probe} is the single
|
||||
* availability signal (a missing binary probes `unusable` the same way an
|
||||
* unenforcing kernel does).
|
||||
* @param resolvePackageJson - test seam over `require.resolve` (the default
|
||||
* covers real installs); receives the platform package's `package.json`
|
||||
* specifier and returns its absolute path, throwing when unresolvable.
|
||||
* @returns the absolute launcher path to probe and exec.
|
||||
*/
|
||||
export function launcherPath(
|
||||
resolvePackageJson: (specifier: string) => string = createRequire(import.meta.url).resolve,
|
||||
): string {
|
||||
const platformPackage = `node-addon-landlock-run-${process.platform}-${process.arch}`
|
||||
try {
|
||||
return join(dirname(resolvePackageJson(`${platformPackage}/package.json`)), 'bin', LAUNCHER_BIN)
|
||||
} catch {
|
||||
// Unresolvable platform package: no such package exists for this host, or
|
||||
// it was not installed. Fall back to the path pnpm's layout WOULD use —
|
||||
// absolute, inside this package's boundary (never cwd-relative: a
|
||||
// spawnable relative path here would hand cwd control over which binary
|
||||
// confines), and nonexistent exactly when the package is absent.
|
||||
return fileURLToPath(new URL(`../node_modules/${platformPackage}/bin/${LAUNCHER_BIN}`, import.meta.url))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The launcher grant arguments for one set of filesystem grants — everything
|
||||
* before the `--` argv separator. A caller spawns
|
||||
* `[launcherPath(), ...grantArgs(grants), '--', ...command]`; the flag
|
||||
* spellings stay private to this package.
|
||||
* @param grants - the read-only and read-write roots to allow.
|
||||
* @returns the `--ro <path>` / `--rw <path>` argument list, read-only roots
|
||||
* first, in the caller's order.
|
||||
*/
|
||||
export function grantArgs(grants: LauncherGrants): string[] {
|
||||
return [
|
||||
...(grants.readOnly ?? []).flatMap(root => ['--ro', root]),
|
||||
...(grants.readWrite ?? []).flatMap(root => ['--rw', root]),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional probe: `landlock-run --probe` builds and enforces a maximal
|
||||
* ruleset in a short-lived child and exits 0 only when the running kernel
|
||||
* actually enforces it — `--version`-style checks would miss a kernel that
|
||||
* has the syscalls but refuses enforcement. The probe's one report line is
|
||||
* part of the CLI contract and distinguishes complete from per-ABI-subset
|
||||
* enforcement; a zero exit without the partial marker reads as `full`. A
|
||||
* failed or timed-out spawn (missing binary, wrong architecture, unenforcing
|
||||
* kernel) probes `unusable`. Synchronous by design: consumers run it once
|
||||
* and cache the verdict.
|
||||
* @param launcher - the launcher path to probe; defaults to
|
||||
* {@link launcherPath}'s resolution for this host.
|
||||
* @param options - `timeoutMs` bounds the probe child (default 2000).
|
||||
* @returns the enforcement verdict for this host.
|
||||
*/
|
||||
export function probe(
|
||||
launcher: string = launcherPath(),
|
||||
options: { timeoutMs?: number } = {},
|
||||
): LandlockEnforcement {
|
||||
const result = spawnSync(launcher, ['--probe'], {
|
||||
timeout: options.timeoutMs ?? 2000,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
})
|
||||
if (result.status !== 0) return 'unusable'
|
||||
return /partially enforced/.test(result.stdout) ? 'partial' : 'full'
|
||||
}
|
||||
302
native/landlock-run/packages/entry/src/main.c
Normal file
302
native/landlock-run/packages/entry/src/main.c
Normal file
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* landlock-run: self-restrict-then-exec Landlock launcher.
|
||||
*
|
||||
* The Landlock rung of a consuming sandbox seam, for Linux hosts where
|
||||
* `bwrap` is
|
||||
* unusable (not installed, unprivileged user namespaces disabled, or an LSM
|
||||
* profile that denies mount — Landlock is an independent syscall family and
|
||||
* needs none of those). The launcher installs a Landlock
|
||||
* ruleset on itself and `exec`s the wrapped command; the ruleset is inherited
|
||||
* across `execve`, so the command (and every process it spawns) runs confined
|
||||
* while the invoking process stays unrestricted.
|
||||
*
|
||||
* CLI contract (mirrors the `bwrap` runner argv shape the executor wraps):
|
||||
*
|
||||
* landlock-run [--ro <path>]... [--rw <path>]... -- <argv>...
|
||||
* landlock-run --probe
|
||||
*
|
||||
* `--ro` grants read+execute beneath the path; `--rw` grants full filesystem
|
||||
* access beneath the path. Everything else is denied (Landlock is an
|
||||
* allow-list). `--probe` builds a maximal ruleset and reports whether the
|
||||
* running kernel actually enforces it — the executor's functional probe.
|
||||
*
|
||||
* Fail-closed: if the ruleset cannot be created or is NOT enforced by the
|
||||
* kernel, the launcher exits non-zero WITHOUT exec'ing the command. A partial
|
||||
* (best-effort) enforcement on an older ABI is accepted and reported on
|
||||
* stderr; the consumer's mode vocabulary keeps its file-effect promises
|
||||
* honest per ABI level (surfaced as `full` vs `partial` by the entry
|
||||
* package's probe).
|
||||
*
|
||||
* Plain C11 over the raw Landlock UAPI — no libraries beyond libc (musl,
|
||||
* linked statically), so the whole audit surface is this file plus the
|
||||
* kernel's stable syscall contract. Built natively per architecture by
|
||||
* `scripts/build.ts` into the per-platform npm packages
|
||||
* (`node-addon-landlock-run-linux-{x64,arm64}`); the argv grammar,
|
||||
* exit codes, and report lines are pinned in `docs/cli-contract.md`.
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/*
|
||||
* The Landlock UAPI, defined locally instead of via <linux/landlock.h>: the
|
||||
* kernel's user-space ABI is stable by contract, self-defining it keeps the
|
||||
* build independent of the toolchain's header vintage, and the definitions
|
||||
* double as the audit record of exactly which kernel surface this launcher
|
||||
* touches. Layouts and values are verbatim from the kernel header (the
|
||||
* path-beneath struct is packed there, so it must be packed here).
|
||||
*/
|
||||
struct landlock_ruleset_attr {
|
||||
uint64_t handled_access_fs;
|
||||
};
|
||||
|
||||
struct landlock_path_beneath_attr {
|
||||
uint64_t allowed_access;
|
||||
int32_t parent_fd;
|
||||
} __attribute__((packed));
|
||||
|
||||
#define LANDLOCK_CREATE_RULESET_VERSION (1U << 0)
|
||||
#define LANDLOCK_RULE_PATH_BENEATH 1
|
||||
|
||||
/* Filesystem access bits, grouped by the Landlock ABI that introduced them. */
|
||||
#define LL_FS_EXECUTE (UINT64_C(1) << 0) /* ABI 1 */
|
||||
#define LL_FS_WRITE_FILE (UINT64_C(1) << 1)
|
||||
#define LL_FS_READ_FILE (UINT64_C(1) << 2)
|
||||
#define LL_FS_READ_DIR (UINT64_C(1) << 3)
|
||||
#define LL_FS_REMOVE_DIR (UINT64_C(1) << 4)
|
||||
#define LL_FS_REMOVE_FILE (UINT64_C(1) << 5)
|
||||
#define LL_FS_MAKE_CHAR (UINT64_C(1) << 6)
|
||||
#define LL_FS_MAKE_DIR (UINT64_C(1) << 7)
|
||||
#define LL_FS_MAKE_REG (UINT64_C(1) << 8)
|
||||
#define LL_FS_MAKE_SOCK (UINT64_C(1) << 9)
|
||||
#define LL_FS_MAKE_FIFO (UINT64_C(1) << 10)
|
||||
#define LL_FS_MAKE_BLOCK (UINT64_C(1) << 11)
|
||||
#define LL_FS_MAKE_SYM (UINT64_C(1) << 12)
|
||||
#define LL_FS_REFER (UINT64_C(1) << 13) /* ABI 2 */
|
||||
#define LL_FS_TRUNCATE (UINT64_C(1) << 14) /* ABI 3 (ABI 4 added TCP bits only) */
|
||||
#define LL_FS_IOCTL_DEV (UINT64_C(1) << 15) /* ABI 5 */
|
||||
|
||||
#define LL_ABI1_MASK (LL_FS_REFER - 1) /* bits 0..12: every ABI-1 access, nothing newer */
|
||||
|
||||
/*
|
||||
* Newest ABI this build knows; the negotiation below scales the actual
|
||||
* ruleset down to what the running kernel supports (the best-effort compat
|
||||
* stance of the previous Rust launcher, made explicit).
|
||||
*/
|
||||
#define MAX_ABI 5L
|
||||
|
||||
/*
|
||||
* Landlock has no libc wrappers; these are the raw syscalls. The numbers are
|
||||
* identical on every architecture (the post-2011 unified table) — the
|
||||
* fallbacks only matter to a libc older than the feature.
|
||||
*/
|
||||
#ifndef __NR_landlock_create_ruleset
|
||||
#define __NR_landlock_create_ruleset 444
|
||||
#define __NR_landlock_add_rule 445
|
||||
#define __NR_landlock_restrict_self 446
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Every fatal launcher error prints `landlock-run: <message>` to stderr
|
||||
* and exits 125 — a code the wrapped command itself is unlikely to use, so
|
||||
* the executor can tell launcher failures from command failures.
|
||||
*/
|
||||
#define EXIT_LAUNCHER_FAILURE 125
|
||||
|
||||
static const char NOT_ENFORCED_MESSAGE[] =
|
||||
"landlock is not enforced by this kernel (ABI unsupported or disabled)";
|
||||
|
||||
/* Print one fatal `landlock-run: ...` line; returns the fatal exit code. */
|
||||
static int fail(const char *prefix, const char *detail) {
|
||||
if (detail == NULL) {
|
||||
fprintf(stderr, "landlock-run: %s\n", prefix);
|
||||
} else {
|
||||
fprintf(stderr, "landlock-run: %s: %s\n", prefix, detail);
|
||||
}
|
||||
return EXIT_LAUNCHER_FAILURE;
|
||||
}
|
||||
|
||||
static int fail_usage(const char *message, const char *detail) {
|
||||
fprintf(stderr, "landlock-run: usage error: %s%s\n", message, detail == NULL ? "" : detail);
|
||||
return EXIT_LAUNCHER_FAILURE;
|
||||
}
|
||||
|
||||
/* Parsed CLI: either a probe, or grants plus the command argv after `--`. */
|
||||
struct cli {
|
||||
int probe;
|
||||
const char **ro;
|
||||
size_t ro_count;
|
||||
const char **rw;
|
||||
size_t rw_count;
|
||||
char **command; /* NULL-terminated tail of main's argv */
|
||||
};
|
||||
|
||||
/*
|
||||
* Hand-rolled argv parsing — four flags do not justify a parsing library,
|
||||
* and the previous Rust launcher made the same call for the same reason.
|
||||
* Returns 0 on success, else the process exit code (message already printed).
|
||||
*/
|
||||
static int parse(int argc, char **argv, struct cli *cli) {
|
||||
/* argc bounds each grant list; the launcher execs or exits, so no free. */
|
||||
cli->ro = calloc(argc > 0 ? (size_t)argc : 1, sizeof *cli->ro);
|
||||
cli->rw = calloc(argc > 0 ? (size_t)argc : 1, sizeof *cli->rw);
|
||||
if (cli->ro == NULL || cli->rw == NULL) return fail("out of memory", NULL);
|
||||
|
||||
int index = 1;
|
||||
while (index < argc) {
|
||||
const char *arg = argv[index];
|
||||
if (strcmp(arg, "--probe") == 0) {
|
||||
cli->probe = 1;
|
||||
index += 1;
|
||||
} else if (strcmp(arg, "--ro") == 0 || strcmp(arg, "--rw") == 0) {
|
||||
if (index + 1 >= argc) {
|
||||
return fail_usage(arg, " requires a path");
|
||||
}
|
||||
if (strcmp(arg, "--ro") == 0) {
|
||||
cli->ro[cli->ro_count++] = argv[index + 1];
|
||||
} else {
|
||||
cli->rw[cli->rw_count++] = argv[index + 1];
|
||||
}
|
||||
index += 2;
|
||||
} else if (strcmp(arg, "--") == 0) {
|
||||
cli->command = &argv[index + 1];
|
||||
break;
|
||||
} else {
|
||||
return fail_usage("unknown argument: ", arg);
|
||||
}
|
||||
}
|
||||
if (cli->probe) {
|
||||
if (cli->ro_count > 0 || cli->rw_count > 0 || (cli->command != NULL && cli->command[0] != NULL)) {
|
||||
return fail_usage("--probe takes no other arguments", NULL);
|
||||
}
|
||||
} else if (cli->command == NULL || cli->command[0] == NULL) {
|
||||
return fail_usage("missing `-- <argv>...` command", NULL);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The filesystem accesses the running kernel's ABI can govern. */
|
||||
static uint64_t fs_mask_for_abi(long abi) {
|
||||
uint64_t mask = LL_ABI1_MASK;
|
||||
if (abi >= 2) mask |= LL_FS_REFER;
|
||||
if (abi >= 3) mask |= LL_FS_TRUNCATE;
|
||||
if (abi >= 5) mask |= LL_FS_IOCTL_DEV;
|
||||
return mask;
|
||||
}
|
||||
|
||||
/* Add one path-beneath rule; 0 on success, else the exit code. */
|
||||
static int add_rule(int ruleset_fd, const char *path, uint64_t access) {
|
||||
int path_fd = open(path, O_PATH | O_CLOEXEC);
|
||||
if (path_fd < 0) {
|
||||
/* Fail closed on an unopenable grant root: silently narrowing the
|
||||
* granted set would be safe, but running with a profile the caller did
|
||||
* not get is not worth the ambiguity. */
|
||||
fprintf(stderr, "landlock-run: cannot open rule path: %s: %s\n", path, strerror(errno));
|
||||
return EXIT_LAUNCHER_FAILURE;
|
||||
}
|
||||
/* The kernel rejects directory-only accesses on a non-directory rule
|
||||
* (EINVAL), so a file grant keeps only the file-compatible bits — how the
|
||||
* `--rw /dev/null` grant works. Same clamp the Rust crate's
|
||||
* path_beneath_rules helper applied. */
|
||||
struct stat st;
|
||||
if (fstat(path_fd, &st) == 0 && !S_ISDIR(st.st_mode)) {
|
||||
access &= LL_FS_EXECUTE | LL_FS_WRITE_FILE | LL_FS_READ_FILE | LL_FS_TRUNCATE | LL_FS_IOCTL_DEV;
|
||||
}
|
||||
struct landlock_path_beneath_attr attr = { .allowed_access = access, .parent_fd = path_fd };
|
||||
if (syscall(__NR_landlock_add_rule, ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &attr, 0) != 0) {
|
||||
int saved = errno;
|
||||
close(path_fd);
|
||||
return fail("landlock ruleset error", strerror(saved));
|
||||
}
|
||||
close(path_fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Install the ruleset on the current thread, negotiating the kernel's ABI
|
||||
* down from MAX_ABI. `--ro` paths get the read side of the vocabulary (read
|
||||
* file/dir + execute — the wrapped `bash` and everything it spawns must
|
||||
* remain executable); `--rw` paths get every filesystem access the
|
||||
* negotiated ABI can grant. Sets `no_new_privs` first (mandatory for an
|
||||
* unprivileged restrict, and it neutralizes setuid/setgid escalation inside
|
||||
* the sandbox). On success `*partial` reports whether the kernel governs
|
||||
* only a subset of MAX_ABI's accesses. Returns 0, else the exit code.
|
||||
*/
|
||||
static int restrict_self(const struct cli *cli, int *partial) {
|
||||
long abi = syscall(__NR_landlock_create_ruleset, NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
|
||||
if (abi < 0) {
|
||||
/* ENOSYS: kernel built without Landlock; EOPNOTSUPP: built but disabled.
|
||||
* Either way: not enforceable — fail CLOSED, never exec unconfined. */
|
||||
return fail(NOT_ENFORCED_MESSAGE, NULL);
|
||||
}
|
||||
*partial = abi < MAX_ABI;
|
||||
uint64_t handled = fs_mask_for_abi(abi < MAX_ABI ? abi : MAX_ABI);
|
||||
|
||||
struct landlock_ruleset_attr attr = { .handled_access_fs = handled };
|
||||
int ruleset_fd = (int)syscall(__NR_landlock_create_ruleset, &attr, sizeof attr, 0);
|
||||
if (ruleset_fd < 0) return fail("landlock ruleset error", strerror(errno));
|
||||
|
||||
const uint64_t read_side = LL_FS_EXECUTE | LL_FS_READ_FILE | LL_FS_READ_DIR;
|
||||
for (size_t i = 0; i < cli->ro_count; i++) {
|
||||
int code = add_rule(ruleset_fd, cli->ro[i], read_side & handled);
|
||||
if (code != 0) return code;
|
||||
}
|
||||
for (size_t i = 0; i < cli->rw_count; i++) {
|
||||
int code = add_rule(ruleset_fd, cli->rw[i], handled);
|
||||
if (code != 0) return code;
|
||||
}
|
||||
|
||||
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
|
||||
return fail("landlock ruleset error", strerror(errno));
|
||||
}
|
||||
if (syscall(__NR_landlock_restrict_self, ruleset_fd, 0) != 0) {
|
||||
return fail("landlock ruleset error", strerror(errno));
|
||||
}
|
||||
close(ruleset_fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
struct cli cli = { 0 };
|
||||
int code = parse(argc, argv, &cli);
|
||||
if (code != 0) return code;
|
||||
|
||||
if (cli.probe) {
|
||||
/* The functional probe: build and enforce a maximal ruleset in THIS
|
||||
* short-lived process (the probe run exits right after). `--version`
|
||||
* style checks would miss a kernel that has the syscalls but refuses
|
||||
* enforcement; actually restricting is the only honest signal. The one
|
||||
* report line is part of the launcher CLI contract — the executor reads
|
||||
* enforcement completeness from it. */
|
||||
static const char *probe_root = "/";
|
||||
struct cli probe = { .ro = &probe_root, .ro_count = 1 };
|
||||
int partial = 0;
|
||||
code = restrict_self(&probe, &partial);
|
||||
if (code != 0) return code;
|
||||
printf("landlock: %s\n", partial ? "partially enforced (older ABI)" : "fully enforced");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int partial = 0;
|
||||
code = restrict_self(&cli, &partial);
|
||||
if (code != 0) return code;
|
||||
if (partial) {
|
||||
/* Older ABI: some handled accesses are not governed (e.g. truncate
|
||||
* before ABI 3). Still confined for everything the kernel supports —
|
||||
* report, do not refuse. */
|
||||
fprintf(stderr, "landlock-run: partial enforcement (older Landlock ABI)\n");
|
||||
}
|
||||
|
||||
execvp(cli.command[0], cli.command);
|
||||
/* exec only returns on failure. */
|
||||
return fail("exec failed", strerror(errno));
|
||||
}
|
||||
11
native/landlock-run/packages/entry/tsconfig.json
Normal file
11
native/landlock-run/packages/entry/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"outDir": "lib",
|
||||
"rootDir": "src",
|
||||
"tsBuildInfoFile": "lib/.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
28
native/landlock-run/packages/linux-arm64/LICENSE
Normal file
28
native/landlock-run/packages/linux-arm64/LICENSE
Normal file
@@ -0,0 +1,28 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2026, node-addon-landlock-run contributors
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
7
native/landlock-run/packages/linux-arm64/README.md
Normal file
7
native/landlock-run/packages/linux-arm64/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# node-addon-landlock-run-linux-arm64
|
||||
|
||||
Prebuilt `bin/landlock-run` Landlock launcher for linux-arm64 — a static musl binary compiled natively (no cross toolchain) from the C source shipped in [`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run). npm's `os`/`cpu` fields select this package at install time; the entry package resolves it to a file path — it ships no JavaScript and is never imported.
|
||||
|
||||
The binary is git-ignored and rides the npm tarball via the `files` list; the `prepack` gate refuses to pack when it is missing or has the wrong ELF architecture, and the release pipeline byte-pins the packed binary against the CI build it came from. Static musl linking means one binary for glibc and musl distros alike — hence no libc suffix in the name.
|
||||
|
||||
Sibling: `node-addon-landlock-run-linux-x64`.
|
||||
26
native/landlock-run/packages/linux-arm64/package.json
Normal file
26
native/landlock-run/packages/linux-arm64/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "node-addon-landlock-run-linux-arm64",
|
||||
"version": "0.0.1",
|
||||
"description": "Prebuilt landlock-run Landlock launcher binary for linux-arm64 (static musl) — resolved as a file path by node-addon-landlock-run, never imported",
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"files": [
|
||||
"README.md",
|
||||
"bin/",
|
||||
"prebuilds.json"
|
||||
],
|
||||
"scripts": {
|
||||
"prepack": "node ../../scripts/verify-launcher-binary.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
10
native/landlock-run/packages/linux-arm64/prebuilds.json
Normal file
10
native/landlock-run/packages/linux-arm64/prebuilds.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"platform": "linux-arm64",
|
||||
"binaries": [
|
||||
{
|
||||
"tool": "landlock-run",
|
||||
"kind": "static-musl",
|
||||
"path": "bin/landlock-run"
|
||||
}
|
||||
]
|
||||
}
|
||||
28
native/landlock-run/packages/linux-x64/LICENSE
Normal file
28
native/landlock-run/packages/linux-x64/LICENSE
Normal file
@@ -0,0 +1,28 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2026, node-addon-landlock-run contributors
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
7
native/landlock-run/packages/linux-x64/README.md
Normal file
7
native/landlock-run/packages/linux-x64/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# node-addon-landlock-run-linux-x64
|
||||
|
||||
Prebuilt `bin/landlock-run` Landlock launcher for linux-x64 — a static musl binary compiled natively (no cross toolchain) from the C source shipped in [`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run). npm's `os`/`cpu` fields select this package at install time; the entry package resolves it to a file path — it ships no JavaScript and is never imported.
|
||||
|
||||
The binary is git-ignored and rides the npm tarball via the `files` list; the `prepack` gate refuses to pack when it is missing or has the wrong ELF architecture, and the release pipeline byte-pins the packed binary against the CI build it came from. Static musl linking means one binary for glibc and musl distros alike — hence no libc suffix in the name.
|
||||
|
||||
Sibling: `node-addon-landlock-run-linux-arm64`.
|
||||
26
native/landlock-run/packages/linux-x64/package.json
Normal file
26
native/landlock-run/packages/linux-x64/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "node-addon-landlock-run-linux-x64",
|
||||
"version": "0.0.1",
|
||||
"description": "Prebuilt landlock-run Landlock launcher binary for linux-x64 (static musl) — resolved as a file path by node-addon-landlock-run, never imported",
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"files": [
|
||||
"README.md",
|
||||
"bin/",
|
||||
"prebuilds.json"
|
||||
],
|
||||
"scripts": {
|
||||
"prepack": "node ../../scripts/verify-launcher-binary.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
10
native/landlock-run/packages/linux-x64/prebuilds.json
Normal file
10
native/landlock-run/packages/linux-x64/prebuilds.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"platform": "linux-x64",
|
||||
"binaries": [
|
||||
{
|
||||
"tool": "landlock-run",
|
||||
"kind": "static-musl",
|
||||
"path": "bin/landlock-run"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user