/* * lbexec.c - per-command Landlock exec wrapper * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause * * Usage: lbexec [-p standard|cargo] [args...] * * Called from build.sh when lockbox=(make meson ninja cargo ...) is set. * mkpkg rewrites matching build commands as: * lbexec (standard profile) * lbexec -p cargo (cargo profile) * * Profiles: * standard full base toolchain + declared deps (default) * cargo base + declared deps, /etc/ssl only, restricted * $CARGO_HOME, CLONE_NEWNET * * Environment (exported by mkpkg into build.sh): * MKPKG_DEPENDS space-separated runtime deps * MKPKG_MAKEDEPENDS space-separated build-time deps * MKPKG_WORK_DIR build work directory (RW) * SRC work/src (RW) * PKG work/pkg (RW) * PKGMK_SOURCE_DIR port directory (RW) * CARGO_HOME cargo home (cargo profile) (RO, selective) * * The parent shell is not restricted -- only the child process and its * children run under Landlock. */ /* _GNU_SOURCE for unshare() and CLONE_NEWNET */ #define _GNU_SOURCE #include "lockbox_common.h" #include "mkpkg.h" #include #include #include #include #include #include #include #include #include #include /* ── Network namespace isolation ─────────────────────────────────────── */ /* * Move into a new network namespace so the child process has no external * network connectivity. Loopback is brought up so localhost still works. * * Requires root (or CONFIG_USER_NS for unprivileged unshare). * Degrades silently — the caller always continues. */ static void try_unshare_net(void) { struct ifreq ifr; int sock; if (unshare(CLONE_NEWNET) != 0) return; /* no capability or no kernel support — degrade silently */ /* bring loopback up so localhost resolution still works */ sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) return; memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, "lo", IFNAMSIZ - 1); if (ioctl(sock, SIOCGIFFLAGS, &ifr) == 0) { ifr.ifr_flags |= IFF_UP | IFF_LOOPBACK; ioctl(sock, SIOCSIFFLAGS, &ifr); } close(sock); fprintf(stderr, "mkpkg: lbexec: network namespace isolated\n"); } /* ── Parse space-separated env var into names array ──────────────────── */ static int parse_env_list(const char *varname, char **out, int out_max) { const char *val = getenv(varname); char *buf, *tok, *saveptr; int count = 0; if (!val || !*val) return 0; buf = strdup(val); if (!buf) return 0; tok = strtok_r(buf, " \t", &saveptr); while (tok && count < out_max) { out[count] = strdup(tok); if (out[count]) count++; tok = strtok_r(NULL, " \t", &saveptr); } free(buf); return count; } /* ── main ────────────────────────────────────────────────────────────── */ int main(int argc, char *argv[]) { int abi, ruleset_fd; uint64_t all_rw; lockbox_profile_t profile = LOCKBOX_PROFILE_STANDARD; int cmd_start; int dep_count; char *dep_names[256]; struct landlock_ruleset_attr rs_attr = {0}; /* parse optional -p */ cmd_start = 1; if (argc >= 3 && strcmp(argv[1], "-p") == 0) { if (strcmp(argv[2], "cargo") == 0) profile = LOCKBOX_PROFILE_CARGO; else if (strcmp(argv[2], "standard") == 0) profile = LOCKBOX_PROFILE_STANDARD; else { fprintf(stderr, "lbexec: unknown profile '%s'\n", argv[2]); return 1; } cmd_start = 3; } if (argc <= cmd_start) { fprintf(stderr, "usage: lbexec [-p standard|cargo] [args...]\n"); return 1; } /* probe Landlock ABI */ abi = (int)syscall(SYS_landlock_create_ruleset, (void *)NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); if (abi < 1) { fprintf(stderr, "mkpkg: error: lbexec: Landlock not available (abi=%d): %s\n", abi, strerror(errno)); return 1; } fprintf(stderr, "mkpkg: lbexec: Landlock ABI v%d, profile: %s\n", abi, profile == LOCKBOX_PROFILE_CARGO ? "cargo" : "standard"); all_rw = ACCESS_RW; if (abi < 3) all_rw &= ~(uint64_t)LANDLOCK_ACCESS_FS_TRUNCATE; /* create ruleset */ rs_attr.handled_access_fs = ACCESS_RO | all_rw; ruleset_fd = ll_create_ruleset(&rs_attr, sizeof(rs_attr), 0); if (ruleset_fd < 0) { fprintf(stderr, "mkpkg: error: lbexec: create_ruleset: %s\n", strerror(errno)); return 1; } /* collect declared deps from environment */ dep_count = 0; dep_count += parse_env_list("MKPKG_MAKEDEPENDS", dep_names + dep_count, 256 - dep_count); dep_count += parse_env_list("MKPKG_DEPENDS", dep_names + dep_count, 256 - dep_count); if (profile == LOCKBOX_PROFILE_CARGO) { /* network isolation first — before filesystem restriction */ try_unshare_net(); if (lb_setup_cargo_ruleset(ruleset_fd, all_rw, (const char **)dep_names, dep_count) != 0) { close(ruleset_fd); return 1; } } else { /* standard profile: full base toolchain + declared deps */ int i; int base_count = 0; int total; const char **names; const char *build_paths[] = { getenv("MKPKG_WORK_DIR"), getenv("SRC"), getenv("PKG"), getenv("PKGMK_SOURCE_DIR"), "/tmp", NULL }; while (lockbox_base[base_count]) base_count++; total = base_count + dep_count; names = malloc((size_t)total * sizeof(char *)); if (!names) { fprintf(stderr, "mkpkg: error: lbexec: malloc: %s\n", strerror(errno)); close(ruleset_fd); return 1; } for (i = 0; i < base_count; i++) names[i] = lockbox_base[i]; for (i = 0; i < dep_count; i++) names[base_count + i] = dep_names[i]; if (lb_add_rules_for_packages(ruleset_fd, names, total, base_count) != 0) { free(names); close(ruleset_fd); return 1; } free(names); for (i = 0; build_paths[i]; i++) { if (!build_paths[i][0]) continue; if (ll_add_path(ruleset_fd, build_paths[i], all_rw) != 0) { fprintf(stderr, "mkpkg: error: lbexec: ll_add_path '%s': %s\n", build_paths[i], strerror(errno)); close(ruleset_fd); return 1; } } if (ll_add_path(ruleset_fd, "/dev", all_rw) != 0 || ll_add_path(ruleset_fd, "/proc", ACCESS_RO) != 0 || ll_add_path(ruleset_fd, "/etc", ACCESS_RO) != 0) { fprintf(stderr, "mkpkg: error: lbexec: ll_add_path pseudo-fs: %s\n", strerror(errno)); close(ruleset_fd); return 1; } } /* engage Landlock */ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) { fprintf(stderr, "mkpkg: error: lbexec: PR_SET_NO_NEW_PRIVS: %s\n", strerror(errno)); close(ruleset_fd); return 1; } if (ll_restrict_self(ruleset_fd) != 0) { fprintf(stderr, "mkpkg: error: lbexec: restrict_self: %s\n", strerror(errno)); close(ruleset_fd); return 1; } close(ruleset_fd); fprintf(stderr, "mkpkg: lbexec: sandbox engaged, executing: %s\n", argv[cmd_start]); execvp(argv[cmd_start], argv + cmd_start); fprintf(stderr, "mkpkg: error: lbexec: execvp '%s': %s\n", argv[cmd_start], strerror(errno)); return 127; }