Resolve security audit findings
ober
3123d77c3248d5b753b02d49a322b6ece39b0af0
--- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: build-test-audit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Install system tools run: | --- a/.github/workflows/security-baseline.yml +++ b/.github/workflows/security-baseline.yml @@ -13,7 +13,7 @@ jobs: baseline: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Required release files run: | --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ LIBS ?= SHIM = jerboa_inotify_shim.so UNAME_S := $(shell uname -s) -.PHONY: all build transpile test test-linux clean shim ensure-jerboa-tools audit \ +.PHONY: all build transpile test test-linux test-loader-policy clean shim ensure-jerboa-tools audit \ platform-check platform-evidence security sbom reproducibility-report verify release-evidence all: build @@ -51,11 +51,16 @@ test: transpile echo "Skipping inotify runtime tests on $(UNAME_S); CI runs them on Linux."; \ fi -test-linux: build - JERBOA_INOTIFY_LIB=$(CURDIR) \ +test-linux: build test-loader-policy + JERBOA_INOTIFY_DEV_NATIVE=1 JERBOA_INOTIFY_LIB=$(CURDIR) \ LD_LIBRARY_PATH=$(CURDIR) \ $(JERBUILD) exec --libdirs "$(LIBDIRS)" tests/inotify-test.ss +test-loader-policy: build + @REPO_ROOT="$(CURDIR)" JERBUILD="$(JERBUILD)" \ + LIBDIRS="$(LIBDIRS)" SHIM="$(CURDIR)/$(SHIM)" \ + sh tests/loader-policy.sh + audit: build @echo "==> Platform" @uname -srm --- a/README.md +++ b/README.md @@ -27,6 +27,12 @@ make release-evidence On non-Linux hosts, `make test` transpiles the Jerboa source and skips runtime tests. CI runs the real kernel tests on Ubuntu. +Dynamic builds must set `JERBOA_INOTIFY_LIB` to the absolute path of the +trusted directory containing `jerboa_inotify_shim.so`. The loader rejects +relative paths, current-working-directory searches, bare library names, and a +final shim path that is a symbolic link. Statically linked hosts are detected +from the registered native entry points and do not need this variable. + If `jerbuild` is not installed, the Makefile bootstraps the pinned Jerboa release into `.jerboa/bin`. --- a/jerboa_inotify_shim.c +++ b/jerboa_inotify_shim.c @@ -102,13 +102,14 @@ int jerboa_inotify_poll(int fd, int timeout_ms) { } int jerboa_inotify_close(int fd) { - int rc; - if (fd < 0) return -EINVAL; - do { - rc = close(fd); - } while (rc < 0 && errno == EINTR); - if (rc < 0) return -errno; + /* On Linux the descriptor is consumed by the first close call even when + * EINTR is reported. Retrying can close an unrelated descriptor that was + * allocated with the same number by another thread. */ + if (close(fd) < 0) { + if (errno == EINTR) return 0; + return -errno; + } return 0; } --- a/scripts/security-check.sh +++ b/scripts/security-check.sh @@ -64,6 +64,13 @@ if ! rg -q 'load-shared-object[*]' src || fail=1 fi +if ! test -f tests/loader-policy.sh || + ! grep -q 'JERBOA_INOTIFY_DEV_NATIVE' src/jerboa-inotify.ss || + ! grep -q 'native-loader-validate-library!' src/jerboa-inotify.ss; then + printf '%s\n' "inotify shared-loader trust policy or regression is missing" >&2 + fail=1 +fi + if ! grep -q 'IN_NONBLOCK | IN_CLOEXEC' jerboa_inotify_shim.c || ! grep -q 'bytes_have_nul' jerboa_inotify_shim.c || ! grep -q 'event_at' jerboa_inotify_shim.c; then @@ -71,6 +78,16 @@ if ! grep -q 'IN_NONBLOCK | IN_CLOEXEC' jerboa_inotify_shim.c || fail=1 fi +if rg -U -q 'do[[:space:]]*\{[^}]*close\([^)]*\)[^}]*\}[[:space:]]*while[^;]*EINTR' jerboa_inotify_shim.c; then + printf '%s\n' "close(2) must never be retried after EINTR" >&2 + fail=1 +fi + +if rg -q '"(\./)?jerboa_inotify_shim[.]so"|JERBOA_INOTIFY_LIB[^\n]*"[.]"' src/jerboa-inotify.ss; then + printf '%s\n' "native shim loading must not search CWD or a bare filename" >&2 + fail=1 +fi + private_pattern='(/Users/|~/mine|\$\(HOME\)/mine|git@|users-MacBook-Pro)' private_matches="$(rg -n -I -e "$private_pattern" . \ --glob '!dist/**' \ --- a/src/jerboa-inotify.ss +++ b/src/jerboa-inotify.ss @@ -21,7 +21,12 @@ IN_IGNORED IN_ISDIR IN_Q_OVERFLOW IN_UNMOUNT) (import (jerboa prelude) - (only (jerboa ffi) c-lambda load-shared-object*)) + (only (jerboa ffi) c-lambda load-shared-object*) + (only (std native-loader) + native-loader-privileged? + native-loader-development-enabled? + native-loader-validate-library! + native-loader-validate-directory!)) ;; Linux uapi values are stable. Literal constants avoid import-time native ;; calls and keep static-binary feature probes safe. @@ -61,23 +66,23 @@ (load-shared-object* path) #t)) - (def (try-load-any! paths) - (let loop ([paths paths]) - (cond - [(null? paths) #f] - [(try-load-one! (car paths)) #t] - [else (loop (cdr paths))]))) - - (def (native-search-paths) - (let ([base (or (getenv "JERBOA_INOTIFY_LIB") ".")]) - (list - (string-append base "/jerboa_inotify_shim.so") - "./jerboa_inotify_shim.so" - "jerboa_inotify_shim.so"))) + (def (configured-native-path) + (let ([base (getenv "JERBOA_INOTIFY_LIB")]) + (unless (native-loader-development-enabled? + "JERBOA_INOTIFY_DEV_NATIVE") + (error 'jerboa-inotify + "dynamic loading requires JERBOA_INOTIFY_DEV_NATIVE=1")) + (native-loader-validate-directory! 'jerboa-inotify base #t) + (let ([path (string-append base "/jerboa_inotify_shim.so")]) + (native-loader-validate-library! 'jerboa-inotify path #t)))) (def (try-load-native!) (or *native-loaded?* - (and (try-load-any! (native-search-paths)) + (and (foreign-entry? "jerboa_inotify_init") + (begin (set! *native-loaded?* #t) #t)) + (and (not (native-loader-privileged?)) + (try-load-one! (configured-native-path)) + (foreign-entry? "jerboa_inotify_init") (begin (set! *native-loaded?* #t) #t)))) new file mode 100644 --- /dev/null +++ b/tests/loader-policy.sh @@ -0,0 +1,66 @@ +#!/bin/sh +set -eu + +: "${REPO_ROOT:?}" +: "${JERBUILD:?}" +: "${LIBDIRS:?}" +: "${SHIM:?}" + +unsafe_tmp=$(mktemp -d "${TMPDIR:-/tmp}/jerboa-inotify-loader.XXXXXXXX") +trusted_tmp=$(mktemp -d "${HOME:?}/.jerboa-inotify-loader.XXXXXXXX") +trap 'rm -rf "$unsafe_tmp" "$trusted_tmp"' EXIT HUP INT TERM +chmod 700 "$trusted_tmp" +probe=$REPO_ROOT/tests/loader-probe.ss +shim_name=$(basename "$SHIM") + +mkdir "$unsafe_tmp/cwd" +cp "$SHIM" "$unsafe_tmp/cwd/$shim_name" +if (cd "$unsafe_tmp/cwd" && env -u JERBOA_INOTIFY_LIB -u JERBOA_INOTIFY_DEV_NATIVE \ + "$JERBUILD" exec --libdirs "$LIBDIRS" "$probe" >/dev/null 2>&1); then + echo "inotify loader accepted a shim from the current directory" >&2 + exit 1 +fi + +if (cd "$unsafe_tmp/cwd" && JERBOA_INOTIFY_DEV_NATIVE=1 JERBOA_INOTIFY_LIB=. \ + "$JERBUILD" exec --libdirs "$LIBDIRS" "$probe" >/dev/null 2>&1); then + echo "inotify loader accepted a relative override" >&2 + exit 1 +fi + +if JERBOA_INOTIFY_LIB="$REPO_ROOT" \ + "$JERBUILD" exec --libdirs "$LIBDIRS" "$probe" >/dev/null 2>&1; then + echo "inotify loader accepted an override without development opt-in" >&2 + exit 1 +fi + +mkdir "$trusted_tmp/real" +cp "$SHIM" "$trusted_tmp/real/$shim_name" +chmod go-w "$trusted_tmp/real" "$trusted_tmp/real/$shim_name" +ln -s "$trusted_tmp/real" "$trusted_tmp/link" +if JERBOA_INOTIFY_DEV_NATIVE=1 JERBOA_INOTIFY_LIB="$trusted_tmp/link" \ + "$JERBUILD" exec --libdirs "$LIBDIRS" "$probe" >/dev/null 2>&1; then + echo "inotify loader accepted a symlink ancestor" >&2 + exit 1 +fi + +mkdir "$trusted_tmp/writable" +cp "$SHIM" "$trusted_tmp/writable/$shim_name" +chmod 777 "$trusted_tmp/writable" +if JERBOA_INOTIFY_DEV_NATIVE=1 JERBOA_INOTIFY_LIB="$trusted_tmp/writable" \ + "$JERBUILD" exec --libdirs "$LIBDIRS" "$probe" >/dev/null 2>&1; then + echo "inotify loader accepted a world-writable ancestor" >&2 + exit 1 +fi + +if test "$(id -u)" -eq 0; then + if JERBOA_INOTIFY_DEV_NATIVE=1 JERBOA_INOTIFY_LIB="$REPO_ROOT" \ + "$JERBUILD" exec --libdirs "$LIBDIRS" "$probe" >/dev/null 2>&1; then + echo "inotify loader honored an environment override as root" >&2 + exit 1 + fi +else + JERBOA_INOTIFY_DEV_NATIVE=1 JERBOA_INOTIFY_LIB="$REPO_ROOT" \ + "$JERBUILD" exec --libdirs "$LIBDIRS" "$probe" >/dev/null +fi + +echo "inotify-loader-policy: ok" new file mode 100644 --- /dev/null +++ b/tests/loader-probe.ss @@ -0,0 +1,5 @@ +(import (jerboa prelude) + (jerboa-inotify)) + +(let ([fd (inotify-init)]) + (inotify-close fd))