From b4fab13fb0c85463c3dae4ec766c1f73e0500033 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Fri, 22 May 2026 23:15:13 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(libexec):=20bklibcvenv=20=E2=80=94=20e?= =?UTF-8?q?xtract=20glibc=20wrapper-script=20pattern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracts the recipe-side hermetic libc-bundling pattern @jhheider just validated inline in pkgxdev/pantry@5354c73f. Recipes that ship their own libc (glibc today; musl, custom libcs tomorrow) get a ~one-line call: - bklibcvenv seal {{prefix}} glibc-{{version.marketing}} instead of the ~20-line inline wrapper-installer that's currently duplicated in the glibc recipe. ## Shape Mirror of libexec/bkpyvenv (Python venv → relocatable stubs): libexec/bklibcvenv — the helper script share/brewkit/libcvenv-wrapper.sh — the POSIX sh wrapper template The helper: 1. takes ` ` as args 2. auto-detects LDSO from `uname -m` (overridable via LDSO env) 3. for each ELF in /{bin,sbin}/* with a PT_INTERP: - moves it to /libexec/-/ - replaces it with a sed-templated sh wrapper 4. wrapper does `cd $(dirname $0)/..` to compute prefix at runtime, then `exec $libdir/$ldso --library-path $libdir $libexec_path "$@"` `` (libdir-name up to first '-') prefixes the libexec subdir so multiple libc bottles can coexist (glibc + musl): - libexec/glibc-bin/ - libexec/glibc-sbin/ ## Why this is useful - **Fully relocatable** — bottle extracted at `/opt`, `~/.pkgx`, or any other path works the same; no absolute paths baked in. - **Sidesteps PT_INTERP** — invoking ld.so explicitly via wrapper ignores the ELF's PT_INTERP. brewkit's fix-elf currently can't rewrite PT_INTERP anyway (pkgxdev/brewkit#345) and the bottle's own ld.so suffers from fix-elf's RPATH pollution when not `build.skip: fix-patchelf`'d. - **Reusable** — same shape works for musl, dietlibc, or any alternate libc anyone packages. ## Connection to bkwinvenv (pkgxdev/brewkit#347) The Windows analog (`bkwinvenv`, sketched at #347) uses the same seal-into-libexec + emit-wrapper-script structure, just with `.cmd` wrappers + Windows DLL search-order semantics (no explicit loader invocation; co-location with the inner .exe is enough). Validating this Linux pattern empirically (which @jhheider's commit does) validates the Windows design too. ## Migration Once this lands, pkgxdev/pantry's glibc recipe can shrink its inline wrapper-installer block (currently ~20 lines + a prop: template) to one line. Same for any future libc recipe. Refs: pkgxdev/brewkit#344 (this RFC), pkgxdev/brewkit#347 (bkwinvenv sibling), pkgxdev/pantry@5354c73f (origin of pattern). --- libexec/bklibcvenv | 106 ++++++++++++++++++++++++++++++ share/brewkit/libcvenv-wrapper.sh | 18 +++++ 2 files changed, 124 insertions(+) create mode 100755 libexec/bklibcvenv create mode 100755 share/brewkit/libcvenv-wrapper.sh diff --git a/libexec/bklibcvenv b/libexec/bklibcvenv new file mode 100755 index 00000000..d9d084a9 --- /dev/null +++ b/libexec/bklibcvenv @@ -0,0 +1,106 @@ +#!/usr/bin/env -S pkgx +nixos.org/patchelf bash +# bklibcvenv — hermetic libc bundler for relocatable libc bottles. +# +# Mirror of libexec/bkpyvenv. Recipe-side helper that makes a libc +# bottle (glibc, eventually musl etc.) ship its bin/* + sbin/* as +# POSIX-sh wrappers that route through the bundled ld.so by relative +# path, so the bottle is fully relocatable. +# +# Usage: +# bklibcvenv seal +# +# Where: +# install root (e.g. {{prefix}}) +# subdir of /lib/ containing libc.so.6 + ld.so +# (e.g. "glibc-2.43"). The script derives: +# LIBC_NAME = libdir-name up to the first '-' +# (so "glibc-2.43" → "glibc") +# LIBC_NAME is used as the libexec subdir prefix +# (libexec/-bin/, libexec/-sbin/) +# so multiple libc bottles can coexist without colliding. +# +# Effect: for each ELF in /{bin,sbin}/* that has a PT_INTERP, +# - move it to /libexec/-/ +# - replace // with a POSIX sh wrapper that: +# * resolves its own bindir (handles invocation by path or PATH) +# * climbs to prefix +# * invokes /lib// --library-path … +# /libexec/-/ "$@" +# +# LDSO is auto-detected from `uname -m`. Override via LDSO env var +# if needed (cross-arch host, exotic loader name, etc.). +# +# Refs: pkgxdev/brewkit#344 (RFC), pkgxdev/pantry@5354c73f (the +# inline-in-glibc-recipe origin of this pattern by @jhheider). + +set -eo pipefail + +CMD=$1; shift + +case "$CMD" in + seal) + if [ $# -lt 2 ]; then + echo "bklibcvenv seal: missing args" >&2 + echo "usage: $0 seal " >&2 + exit 64 + fi + + PREFIX=$1 + LIBDIR_NAME=$2 + + # Auto-detect ld.so name from arch. Override-able via LDSO env. + if [ -z "${LDSO:-}" ]; then + case "$(uname -m)" in + x86_64) LDSO=ld-linux-x86-64.so.2 ;; + aarch64|arm64) LDSO=ld-linux-aarch64.so.1 ;; + armv7*|armhf) LDSO=ld-linux-armhf.so.3 ;; + i686|i386) LDSO=ld-linux.so.2 ;; + *) echo "bklibcvenv: unsupported arch $(uname -m); set LDSO env" >&2; exit 1 ;; + esac + fi + + # libc name from libdir's first dash-separated component. + LIBC_NAME=${LIBDIR_NAME%%-*} + + # Locate the wrapper template (share/brewkit/libcvenv-wrapper.sh). + SELF_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd) + TEMPLATE="$SELF_DIR/../share/brewkit/libcvenv-wrapper.sh" + if [ ! -f "$TEMPLATE" ]; then + echo "bklibcvenv: wrapper template not found at $TEMPLATE" >&2 + exit 2 + fi + + set -x + + for dir in bin sbin; do + [ -d "$PREFIX/$dir" ] || continue + mkdir -p "$PREFIX/libexec/$LIBC_NAME-$dir" + + for f in "$PREFIX/$dir"/*; do + [ -f "$f" ] && [ ! -L "$f" ] || continue + # Skip non-ELF (shell scripts, etc.) — patchelf exits non-zero + # when the file isn't an ELF with a PT_INTERP. + patchelf --print-interpreter "$f" >/dev/null 2>&1 || continue + + name=$(basename "$f") + mv "$f" "$PREFIX/libexec/$LIBC_NAME-$dir/" + + sed \ + -e "s|@LDSO@|$LDSO|g" \ + -e "s|@LIBDIR@|$LIBDIR_NAME|g" \ + -e "s|@LIBC_NAME@|$LIBC_NAME|g" \ + -e "s|@DIR@|$dir|g" \ + "$TEMPLATE" > "$f" + chmod 775 "$f" + + echo "wrapped $f" + done + done + ;; + + *) + echo "bklibcvenv: unknown subcommand '$CMD'" >&2 + echo "usage: $0 seal " >&2 + exit 64 + ;; +esac diff --git a/share/brewkit/libcvenv-wrapper.sh b/share/brewkit/libcvenv-wrapper.sh new file mode 100755 index 00000000..c12ab2a6 --- /dev/null +++ b/share/brewkit/libcvenv-wrapper.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# Template for bklibcvenv-generated wrappers. +# +# Replaced by `bklibcvenv seal`: +# @LDSO@ e.g. ld-linux-x86-64.so.2 +# @LIBDIR@ e.g. glibc-2.43 (subdir of $prefix/lib/) +# @LIBC_NAME@ e.g. glibc (prefix for libexec/-/) +# @DIR@ e.g. bin (or sbin) + +case "$0" in + */*) bindir=${0%/*} ;; + *) bindir=$(command -v -- "$0"); bindir=${bindir%/*} ;; +esac + +prefix=$(CDPATH= cd -- "$bindir/.." && pwd) +libdir="$prefix/lib/@LIBDIR@" + +exec "$libdir/@LDSO@" --library-path "$libdir" "$prefix/libexec/@LIBC_NAME@-@DIR@/$(basename "$0")" "$@" From 9ed9496fd27fb8412117667871f5e73a128b5444 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Fri, 22 May 2026 23:53:20 +0200 Subject: [PATCH 2/2] review(bklibcvenv): address Copilot's 5 inline comments on #348 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 5 points were legit: 1. **shift on empty args** (libexec/bklibcvenv:39) — with set -eo pipefail, `CMD=$1; shift` exited before the usage-error branch could fire when called with zero args. Now: check `$#` first. 2. **set -x unconditional** (line 73) — floods CI output. Gated behind BKLIBCVENV_DEBUG env var. 3. **chmod 775** (line 94) — group-writable on packaged artefacts is a bad-practice flag for security/packaging audits. Changed to 755. 4. **POSIX `--` in command -v / cd** (template line 12, 15) — not specified by POSIX for these built-ins; some old /bin/sh implementations reject. Dropped `--`; in our context $0 is never user-controlled so the defense was moot. 5. **sed-replacement metacharacter unsafety** (line 93) — values containing `&`, `\`, or the `|` delimiter would corrupt sed output. Defense in depth: added a `sed_escape()` helper and applied to all four interpolated values before substitution. Values in question (LDSO, LIBDIR_NAME, LIBC_NAME, dir) are all auto-detected or recipe-passed and won't realistically contain sed metachars, but Copilot's right that this should not be a latent footgun. --- libexec/bklibcvenv | 41 +++++++++++++++++++++++++------ share/brewkit/libcvenv-wrapper.sh | 8 +++--- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/libexec/bklibcvenv b/libexec/bklibcvenv index d9d084a9..5e8618fc 100755 --- a/libexec/bklibcvenv +++ b/libexec/bklibcvenv @@ -35,7 +35,28 @@ set -eo pipefail -CMD=$1; shift +if [ $# -lt 1 ]; then + echo "bklibcvenv: missing subcommand" >&2 + echo "usage: $0 seal " >&2 + exit 64 +fi + +CMD=$1 +shift + +# Optional debug tracing — opt-in via env to avoid log noise. +[ -n "${BKLIBCVENV_DEBUG:-}" ] && set -x + +# sed-replacement-safe escape. Backslashes, ampersands, and the +# `|` delimiter need to be quoted in the replacement-half of an +# `s|…|…|g`. Without escaping a value like `foo&bar` would be +# replaced by sed's match-back-reference; `foo\1bar` similarly +# breaks. In practice our values are tightly controlled (LDSO, +# LIBDIR_NAME etc. come from arch/version, never user input), so +# this is defense in depth. +sed_escape() { + printf '%s\n' "$1" | sed -e 's/[\\&|]/\\&/g' +} case "$CMD" in seal) @@ -63,14 +84,17 @@ case "$CMD" in LIBC_NAME=${LIBDIR_NAME%%-*} # Locate the wrapper template (share/brewkit/libcvenv-wrapper.sh). - SELF_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd) + SELF_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd) TEMPLATE="$SELF_DIR/../share/brewkit/libcvenv-wrapper.sh" if [ ! -f "$TEMPLATE" ]; then echo "bklibcvenv: wrapper template not found at $TEMPLATE" >&2 exit 2 fi - set -x + # Escape sed-replacement values once up-front. + LDSO_ESC=$(sed_escape "$LDSO") + LIBDIR_ESC=$(sed_escape "$LIBDIR_NAME") + LIBC_ESC=$(sed_escape "$LIBC_NAME") for dir in bin sbin; do [ -d "$PREFIX/$dir" ] || continue @@ -85,13 +109,14 @@ case "$CMD" in name=$(basename "$f") mv "$f" "$PREFIX/libexec/$LIBC_NAME-$dir/" + DIR_ESC=$(sed_escape "$dir") sed \ - -e "s|@LDSO@|$LDSO|g" \ - -e "s|@LIBDIR@|$LIBDIR_NAME|g" \ - -e "s|@LIBC_NAME@|$LIBC_NAME|g" \ - -e "s|@DIR@|$dir|g" \ + -e "s|@LDSO@|$LDSO_ESC|g" \ + -e "s|@LIBDIR@|$LIBDIR_ESC|g" \ + -e "s|@LIBC_NAME@|$LIBC_ESC|g" \ + -e "s|@DIR@|$DIR_ESC|g" \ "$TEMPLATE" > "$f" - chmod 775 "$f" + chmod 755 "$f" echo "wrapped $f" done diff --git a/share/brewkit/libcvenv-wrapper.sh b/share/brewkit/libcvenv-wrapper.sh index c12ab2a6..61ae95b3 100755 --- a/share/brewkit/libcvenv-wrapper.sh +++ b/share/brewkit/libcvenv-wrapper.sh @@ -1,5 +1,7 @@ #!/bin/sh -# Template for bklibcvenv-generated wrappers. +# Template for bklibcvenv-generated wrappers. Pure POSIX sh — no +# `--` end-of-options markers (POSIX doesn't specify them for `cd` +# or `command -v`; some old /bin/sh implementations reject them). # # Replaced by `bklibcvenv seal`: # @LDSO@ e.g. ld-linux-x86-64.so.2 @@ -9,10 +11,10 @@ case "$0" in */*) bindir=${0%/*} ;; - *) bindir=$(command -v -- "$0"); bindir=${bindir%/*} ;; + *) bindir=$(command -v "$0"); bindir=${bindir%/*} ;; esac -prefix=$(CDPATH= cd -- "$bindir/.." && pwd) +prefix=$(CDPATH= cd "$bindir/.." && pwd) libdir="$prefix/lib/@LIBDIR@" exec "$libdir/@LDSO@" --library-path "$libdir" "$prefix/libexec/@LIBC_NAME@-@DIR@/$(basename "$0")" "$@"