/* * lockbox_common.c - shared Landlock infrastructure * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause */ #include "lockbox_common.h" #include "util.h" #include #include #include #include #include /* ── Implicit base toolchain ─────────────────────────────────────────── */ /* * Always permitted — these are the minimal tools present in every * rawnix.org build environment that do not need to be listed in makedepends. */ const char *lockbox_base[] = { "llvm", /* clang, lld, llvm-ar, llvm-nm */ "libffi", /* llvm-toolchain dep */ "libedit", /* llvm-toolchain dep */ "ncurses", /* llvm-toolchain dep */ "libxml2", /* libLLVM.so links it */ "musl", /* libc headers and runtime */ "linux-headers", /* /include — CPPFLAGS=-I/include */ "mawk", /* awk for configure scripts */ "bash", /* mkpkg build hooks */ "dash", /* build() runs under POSIX sh (/bin/sh) */ "coreutils", /* cp, mkdir, install, ... */ "gmp", /* coreutils expr links libgmp */ "tar", /* source extraction */ "acl", /* tar + sed link libacl */ "attr", /* tar + sed link libattr */ "libcap2", /* coreutils ls links libcap */ "grep", /* configure scripts */ "pcre2", /* grep links libpcre2-8 */ "sed", /* configure scripts */ "gzip", /* source extraction (.tar.gz) */ "xz", /* source extraction (.tar.xz) */ "bzip2", /* source extraction (.tar.bz2) */ "zstd", /* source extraction (.tar.zst) */ "findutils", /* find used in configure and make */ "diffutils", /* diff used in configure */ "patch", /* patch() in MAKEPKG */ "file", /* configure uses file(1) */ "make", /* GNU make */ "autoconf", /* config.status uses it */ "automake", /* dependency tracking bootstrap */ "libtool", /* libtool scripts in builds */ "m4", /* autoconf/automake use m4 */ "perl", /* autoconf/automake/autoreconf are perl */ "mkpkg", /* mkpkg + addpkg + delpkg + infopkg */ "pkgconf", /* universal configure probe tool */ "libressl", /* mkpkg links libssl/libcrypto */ "signify-openbsd", /* post-build signature step (in-process) */ "zlib", /* mkpkg and builds link libz */ "readline", /* bash links libreadline.so */ NULL }; /* ── lb_add_rules_for_packages ───────────────────────────────────────── */ int lb_add_rules_for_packages(int ruleset_fd, const char **names, int name_count, int base_count) { return lb_add_rules_ex(ruleset_fd, names, name_count, base_count, name_count); } int lb_add_rules_ex(int ruleset_fd, const char **names, int name_count, int base_count, int implied_start) { const char *env = getenv("PKG_DBDIR"); char dbpath[LB_PATH_MAX]; FILE *fp; char line[LB_PATH_MAX]; char current_name[256]; int in_match, match_idx, line_num, rules_added; char *found; snprintf(dbpath, sizeof(dbpath), "%s/db", env && env[0] == '/' ? env : PKG_DBDIR); fp = fopen(dbpath, "r"); if (!fp) { fprintf(stderr, "mkpkg: error: lockbox: cannot open db '%s': %s\n", dbpath, strerror(errno)); return -1; } found = NULL; if (name_count > 0) { /* Track found state for base packages too, not just declared deps. * Base misses are warnings (silent install of mkpkg without one of * its base dep packages was always a deliberate choice on rawnix.org * bootstrap), but a typo or rename should still surface — the * symptom otherwise is "Permission denied" deep inside a build. */ found = calloc((size_t)name_count, 1); if (!found) { fprintf(stderr, "mkpkg: error: lockbox: calloc: %s\n", strerror(errno)); fclose(fp); return -1; } } current_name[0] = '\0'; in_match = 0; match_idx = -1; line_num = 0; rules_added = 0; while (fgets(line, sizeof(line), fp)) { char *nl = strchr(line, '\n'); if (nl) *nl = '\0'; if (line[0] == '\0') { /* blank line: end of record */ current_name[0] = '\0'; in_match = 0; match_idx = -1; line_num = 0; continue; } if (line_num == 0) { int i; snprintf(current_name, sizeof(current_name), "%s", line); in_match = 0; match_idx = -1; for (i = 0; i < name_count; i++) { if (strcmp(current_name, names[i]) == 0) { in_match = 1; if (match_idx < 0) match_idx = i; /* first match sets the rule index */ found[i] = 1; /* no break — pkg may appear in both base and declared deps */ } } line_num++; continue; } if (line_num == 1) { line_num++; continue; } /* version line */ if (in_match) { char fullpath[LB_PATH_MAX]; size_t len; int is_dir; uint64_t access; snprintf(fullpath, sizeof(fullpath), "/%s", line); len = strlen(fullpath); is_dir = (len > 1 && fullpath[len - 1] == '/'); if (is_dir) fullpath[len - 1] = '\0'; /* warn on missing files for declared deps only — an * implied package may legitimately be partly installed */ if (match_idx >= base_count && match_idx < implied_start && !is_dir) { struct stat st; if (stat(fullpath, &st) != 0 && errno == ENOENT) fprintf(stderr, "mkpkg: warning: lockbox: dep '%s' missing: %s\n", current_name, fullpath); } access = is_dir ? (uint64_t)LANDLOCK_ACCESS_FS_READ_DIR : (uint64_t)(LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_EXECUTE); if (ll_add_path(ruleset_fd, fullpath, access) != 0) { fprintf(stderr, "mkpkg: error: lockbox: ll_add_path '%s': %s\n", fullpath, strerror(errno)); free(found); fclose(fp); return -1; } rules_added++; } line_num++; } fclose(fp); /* audit which packages were found in the db */ if (found) { int i, missing = 0; for (i = 0; i < base_count; i++) { if (!found[i]) { fprintf(stderr, "mkpkg: warning: lockbox: base package '%s' " "not in db (renamed? removed?)\n", names[i]); /* not fatal — rawnix.org bootstrap may legitimately * lack some base packages; log and proceed */ } } for (i = base_count; i < implied_start; i++) { if (!found[i]) { fprintf(stderr, "mkpkg: error: lockbox: dep '%s' not installed\n", names[i]); missing++; } } for (i = implied_start; i < name_count; i++) { if (!found[i]) { /* deps.db named it but db does not own it — a stale * edge or a renamed package. Not this port's fault, * so warn and build on. */ fprintf(stderr, "mkpkg: warning: lockbox: implied dep '%s' " "not in db (stale deps.db edge?)\n", names[i]); } } free(found); if (missing > 0) return -1; } fprintf(stderr, "mkpkg: lockbox: %d rules added from mkpkg db\n", rules_added); return 0; } /* ── lb_setup_cargo_ruleset ──────────────────────────────────────────── */ /* * Cargo-profile Landlock setup. * * Allows: * - rust + musl from pkgdb (base, silent if missing) * - declared makedeps/deps from pkgdb (fatal if missing) * - $SRC/ and $PKG/ read-write (build artifacts, install target) * - $CARGO_HOME/ read-only BUT excluding registry/ and git/ * (vendor mode replaces them — we simply don't add those paths) * - /tmp, /dev, /proc, /etc/ssl read as needed * * The caller is responsible for applying CLONE_NEWNET before this * function is called (network namespace blocks outbound connections). */ int lb_setup_cargo_ruleset(int ruleset_fd, uint64_t all_rw, const char **dep_names, int dep_count) { int cargo_base_count = 0; int total; const char **names; int i, ret; const char *cargo_home; char cargo_home_sub[LB_PATH_MAX]; /* subdirectories of $CARGO_HOME to allow (registry/ and git/ excluded) */ static const char *cargo_home_allow[] = { "bin", "env", ".package-cache", "toolchains", NULL }; /* * cargo base = lockbox_base (silent) + declared deps (fatal). * rust comes from depends=(rust) in the MAKEPKG and will be in * dep_names. Do NOT add it here too — lb_add_rules_for_packages * would match it at the base position first and never mark the * declared dep as found, causing a false "not installed" error. */ while (lockbox_base[cargo_base_count]) cargo_base_count++; total = cargo_base_count + dep_count; names = malloc((size_t)total * sizeof(char *)); if (!names) { fprintf(stderr, "mkpkg: error: lockbox: malloc: %s\n", strerror(errno)); return -1; } for (i = 0; i < cargo_base_count; i++) names[i] = lockbox_base[i]; for (i = 0; i < dep_count; i++) names[cargo_base_count + i] = dep_names[i]; ret = lb_add_rules_for_packages(ruleset_fd, names, total, cargo_base_count); free(names); if (ret != 0) return -1; /* $CARGO_HOME — allow specific subdirs only, exclude registry/ and git/ */ cargo_home = getenv("CARGO_HOME"); if (cargo_home && cargo_home[0]) { for (i = 0; cargo_home_allow[i]; i++) { snprintf(cargo_home_sub, sizeof(cargo_home_sub), "%s/%s", cargo_home, cargo_home_allow[i]); ll_add_path(ruleset_fd, cargo_home_sub, ACCESS_RO); } } /* build paths: $SRC, $PKG, $MKPKG_WORK_DIR, $PKGMK_SOURCE_DIR */ { const char *rw_paths[] = { getenv("SRC"), getenv("PKG"), getenv("MKPKG_WORK_DIR"), getenv("PKGMK_SOURCE_DIR"), "/tmp", NULL }; for (i = 0; rw_paths[i]; i++) { if (!rw_paths[i][0]) continue; if (ll_add_path(ruleset_fd, rw_paths[i], all_rw) != 0) { fprintf(stderr, "mkpkg: error: lockbox: cargo: ll_add_path '%s': %s\n", rw_paths[i], strerror(errno)); return -1; } } } /* pseudo-fs and ssl certs */ ll_add_path(ruleset_fd, "/dev", all_rw); ll_add_path(ruleset_fd, "/proc", ACCESS_RO); ll_add_path(ruleset_fd, "/etc/ssl", ACCESS_RO); fprintf(stderr, "mkpkg: lockbox: cargo profile engaged\n"); return 0; } /* ── deps.db closure expansion ───────────────────────────────────────── */ /* * Packages never granted via the implied path. Reaching one of these * sideways, through some other package's depends, is never what a build * wants; a port that genuinely needs one must name it in makedeps. */ static const char *lb_implied_deny[] = { "wget", "wget2", "rsync", "curl", "git", "openssh", NULL }; static int lb_denied(const char *name) { int i; for (i = 0; lb_implied_deny[i]; i++) if (strcmp(name, lb_implied_deny[i]) == 0) return 1; return 0; } /* * Match "key = value" (or "key=value") at the start of s. * Returns a pointer to the value, or NULL if the key does not match. * "dependsfoo = x" does not match the key "depends". */ static const char *lb_ini_value(const char *s, const char *key) { size_t klen = strlen(key); if (strncmp(s, key, klen) != 0) return NULL; s += klen; while (*s == ' ' || *s == '\t') s++; if (*s != '=') return NULL; s++; while (*s == ' ' || *s == '\t') s++; return s; } /* * Load deps.db into two index-aligned arrays: * (*out_names)->items[i] section name * (*out_deps)->items[i] that section's "depends =" value, "" if absent * * Both arrays stay unsorted so the indices line up; the lookup below is a * linear scan, which is fine for a few tens of queries. * * Returns 0 on success, -1 when the file cannot be read. A -1 is not an * error: it means no closure information is available (no pkg installed), * and the caller falls back to declared deps only. */ static int lb_depsdb_load(strarray_t **out_names, strarray_t **out_deps) { const char *env = getenv("PKG_DBDIR"); char path[LB_PATH_MAX]; FILE *fp; char line[LB_PATH_MAX]; char cur_name[256]; char cur_deps[LB_PATH_MAX]; strarray_t *names, *deps; snprintf(path, sizeof(path), "%s/%s", env && env[0] == '/' ? env : PKG_DBDIR, PKG_DEPSFILE); fp = fopen(path, "r"); if (!fp) return -1; names = strarray_new(); deps = strarray_new(); cur_name[0] = '\0'; cur_deps[0] = '\0'; while (fgets(line, sizeof(line), fp)) { char *s = str_trim(line); if (s[0] == '[') { char *end = strchr(s, ']'); if (!end) continue; /* malformed header — skip */ if (cur_name[0]) { strarray_add(names, cur_name); strarray_add(deps, cur_deps); } *end = '\0'; snprintf(cur_name, sizeof(cur_name), "%s", s + 1); cur_deps[0] = '\0'; continue; } if (str_starts_with(s, "FORMAT_VERSION")) { if (strcmp(str_trim(s + 14), "1") != 0) warning("lockbox: %s: unknown FORMAT_VERSION '%s' — " "closure may be wrong", path, str_trim(s + 14)); continue; } if (cur_name[0]) { const char *v = lb_ini_value(s, "depends"); if (v) snprintf(cur_deps, sizeof(cur_deps), "%s", v); } } if (cur_name[0]) { strarray_add(names, cur_name); strarray_add(deps, cur_deps); } fclose(fp); *out_names = names; *out_deps = deps; return 0; } /* ── lb_expand_deps ──────────────────────────────────────────────────── */ strarray_t *lb_expand_deps(const char **declared, int declared_count) { strarray_t *names, *deps; strarray_t *seen, *work, *implied; size_t idx; int i; if (declared_count <= 0) return NULL; if (lb_depsdb_load(&names, &deps) != 0) return NULL; /* no deps.db — no closure */ seen = strarray_new(); work = strarray_new(); implied = strarray_new(); /* declared names seed the walk and are never re-emitted as implied */ for (i = 0; i < declared_count; i++) { if (strarray_contains(seen, declared[i])) continue; strarray_add(seen, declared[i]); strarray_add(work, declared[i]); } /* fixed point: work grows as the closure is discovered, and * work->count is re-read every iteration */ for (idx = 0; idx < work->count; idx++) { const char *dl = NULL; char *buf, *tok, *save; size_t j; for (j = 0; j < names->count; j++) { if (strcmp(names->items[j], work->items[idx]) == 0) { dl = deps->items[j]; break; } } if (!dl || !dl[0]) continue; /* absent, or a leaf */ buf = xstrdup(dl); /* strtok_r writes to it */ for (tok = strtok_r(buf, " \t", &save); tok; tok = strtok_r(NULL, " \t", &save)) { if (strarray_contains(seen, tok)) continue; strarray_add(seen, tok); /* mark before any skip, * so cycles terminate */ if (lb_denied(tok)) { warning("lockbox: implied dep '%s' not granted — " "declare it in makedeps if the build needs it", tok); continue; /* and do not walk its deps */ } strarray_add(work, tok); strarray_add(implied, tok); } free(buf); } strarray_free(names); strarray_free(deps); strarray_free(seen); strarray_free(work); if (implied->count == 0) { strarray_free(implied); return NULL; } return implied; }