/* * lockbox.c - Landlock LSM build isolation for mkpkg * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause * * Restricts the build (in a child process, see lockbox_enter) to: * READ: files owned by lockbox_base + declared deps/makedepends * WRITE: work_dir, source_dir, package_dir, tmp_dir, port dir, /tmp * * Undeclared library probes are blocked at the kernel level — builds * either skip the feature or fail, never silently link an unlisted dep. */ #include "mkpkg.h" #include "lockbox_common.h" #include #include #include #include #include #include #include /* * Runs in the child forked by lockbox_enter(): build the ruleset, * restrict this process, build. Returns the build's exit code. */ static int lockbox_child(pkgfile_t *pkgfile, config_t *config, const char *target) { int abi, ruleset_fd; uint64_t all_rw; int base_count, declared_count, implied_count, total, n, i; const char **names; const char **declared; strarray_t *implied; struct landlock_ruleset_attr rs_attr = {0}; const char *build_paths[8]; int ret; /* probe Landlock ABI version */ abi = (int)syscall(SYS_landlock_create_ruleset, (void *)NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); if (abi < 1) { fprintf(stderr, "mkpkg: error: lockbox: Landlock not available (abi=%d): %s\n" "mkpkg: error: lockbox: kernel >= 5.13 with " "CONFIG_SECURITY_LANDLOCK=y required\n", abi, strerror(errno)); return E_BUILD; } fprintf(stderr, "mkpkg: lockbox: Landlock ABI v%d\n", abi); /* mask TRUNCATE on kernels < 6.2 (ABI v3) */ all_rw = ACCESS_RW; if (abi < 3) all_rw &= ~(uint64_t)LANDLOCK_ACCESS_FS_TRUNCATE; /* create ruleset. Handling the net rights with no NET_PORT rule * added denies both outright: the download phase has already run * in the parent, so a build has no business opening a socket. * Landlock covers TCP only -- UDP (and so DNS) is untouched; the * cargo profile's CLONE_NEWNET is what closes that. */ rs_attr.handled_access_fs = ACCESS_RO | all_rw; #ifdef LANDLOCK_ACCESS_NET_CONNECT_TCP if (abi >= 4) rs_attr.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | LANDLOCK_ACCESS_NET_CONNECT_TCP; #endif ruleset_fd = ll_create_ruleset(&rs_attr, sizeof(rs_attr), 0); if (ruleset_fd < 0) { fprintf(stderr, "mkpkg: error: lockbox: create_ruleset: %s\n", strerror(errno)); return E_BUILD; } /* declared = makedepends + depends, as written in the MAKEPKG */ declared_count = pkgfile->makedepend_count + pkgfile->depend_count; declared = malloc((size_t)(declared_count ? declared_count : 1) * sizeof(char *)); if (!declared) { fprintf(stderr, "mkpkg: error: lockbox: malloc: %s\n", strerror(errno)); close(ruleset_fd); return E_BUILD; } n = 0; for (i = 0; i < pkgfile->makedepend_count; i++) declared[n++] = pkgfile->makedepends[i]; for (i = 0; i < pkgfile->depend_count; i++) declared[n++] = pkgfile->depends[i]; /* implied = transitive closure of the declared names over deps.db. * lockbox_base is deliberately NOT expanded: it is the hand-audited * floor that has to hold on a box with mkpkg and no pkg installed. */ implied = lb_expand_deps(declared, declared_count); implied_count = implied ? (int)implied->count : 0; /* name list: base + declared + implied */ base_count = 0; while (lockbox_base[base_count]) base_count++; total = base_count + declared_count + implied_count; names = malloc((size_t)total * sizeof(char *)); if (!names) { fprintf(stderr, "mkpkg: error: lockbox: malloc: %s\n", strerror(errno)); free(declared); strarray_free(implied); close(ruleset_fd); return E_BUILD; } n = 0; for (i = 0; i < base_count; i++) names[n++] = lockbox_base[i]; for (i = 0; i < declared_count; i++) names[n++] = declared[i]; for (i = 0; i < implied_count; i++) names[n++] = implied->items[i]; /* one pass over db: a name reached twice still emits its files once */ if (lb_add_rules_ex(ruleset_fd, names, total, base_count, base_count + declared_count) != 0) { free(names); free(declared); strarray_free(implied); close(ruleset_fd); return E_BUILD; } free(names); free(declared); if (implied_count > 0) { size_t k; fprintf(stderr, "mkpkg: lockbox: %d implied via deps.db:", implied_count); for (k = 0; k < implied->count; k++) fprintf(stderr, " %s", implied->items[k]); fputc('\n', stderr); } strarray_free(implied); /* add RW rules for build paths */ build_paths[0] = config->work_dir; build_paths[1] = config->source_dir; build_paths[2] = config->package_dir; build_paths[3] = config->tmp_dir; build_paths[4] = config->root_dir; /* port dir: MAKEPKG, patches, sources */ build_paths[5] = "/tmp"; build_paths[6] = NULL; 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: lockbox: ll_add_path '%s': %s\n", build_paths[i], strerror(errno)); close(ruleset_fd); return E_BUILD; } } /* essential pseudo-filesystems */ 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: lockbox: ll_add_path pseudo-fs: %s\n", strerror(errno)); close(ruleset_fd); return E_BUILD; } /* the hooks source the configuration; with -g it may live outside * /etc. Best effort: a missing file is simply not sourced. */ if (config->config_file[0]) (void)ll_add_path(ruleset_fd, config->config_file, LANDLOCK_ACCESS_FS_READ_FILE); /* engage — no new privileges */ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) { fprintf(stderr, "mkpkg: error: lockbox: PR_SET_NO_NEW_PRIVS: %s\n", strerror(errno)); close(ruleset_fd); return E_BUILD; } if (ll_restrict_self(ruleset_fd) != 0) { fprintf(stderr, "mkpkg: error: lockbox: restrict_self: %s\n", strerror(errno)); close(ruleset_fd); return E_BUILD; } close(ruleset_fd); fprintf(stderr, "mkpkg: lockbox: filesystem restricted to declared deps + build paths\n"); #ifdef LANDLOCK_ACCESS_NET_CONNECT_TCP if (abi >= 4) fprintf(stderr, "mkpkg: lockbox: network denied (TCP bind + connect)\n"); #endif /* run the build inside the sandbox -- the caller already made (or, * with -ww, kept) the work directory and removes it afterwards */ ret = build_package(pkgfile, config, target); return ret; } /* * Landlock cannot be lifted once applied, and it is inherited by every * later child. Restricting mkpkg itself therefore also sandboxed what * runs after the build -- footprint, signing, shell() [post] and the * addpkg of -r -- which then failed on paths outside the ruleset. The * sandbox now lives in a child that only builds; mkpkg continues * unrestricted once it exits. */ int lockbox_enter(pkgfile_t *pkgfile, config_t *config, const char *target) { pid_t pid; int status; fflush(stdout); fflush(stderr); pid = fork(); if (pid < 0) { fprintf(stderr, "mkpkg: error: lockbox: fork: %s\n", strerror(errno)); return E_BUILD; } if (pid == 0) { int r = lockbox_child(pkgfile, config, target); fflush(stdout); fflush(stderr); _exit(r); } while (waitpid(pid, &status, 0) < 0) { if (errno != EINTR) { fprintf(stderr, "mkpkg: error: lockbox: waitpid: %s\n", strerror(errno)); return E_BUILD; } } if (WIFEXITED(status)) return WEXITSTATUS(status); return E_BUILD; }