#!/bin/bash # Can a restore actually put back the uid a file had when it was backed up? # # scripts/dev/lp-userns-ownership-test # as the backup user # sudo -u dockerinstall -H scripts/dev/lp-userns-ownership-test # # Restore runs unprivileged, so it reinstates ownership through a user namespace # (scripts/backup/engine/restic-userns-exec). The previous one-line `unshare` # prefix mapped root and the subuid range but not the backup user's own GID — # and app data is stored as :, so the group half of # every chown was unmapped, lchown returned EINVAL, and the file silently kept # the restoring user's ownership. # # Nothing failed. restic reported the misses as "ignoring error …" and exited 0, # and the caller forgave them as expected. The only symptom was grafana coming # back as 1002:1002 instead of 231543:1002 and dying with "attempt to write a # readonly database". # # So the case that matters is the middle one: a container uid with the backup # user as its group. REPO="$(cd "$(dirname "$0")/../.." && pwd)" HELPER="$REPO/scripts/backup/engine/restic-userns-exec" fail=0 chk(){ if [[ "$2" == "$3" ]]; then echo " ok $1"; else echo " FAIL $1: got '$2' want '$3'"; fail=1; fi; } [[ -r "$HELPER" ]] || { echo " FAIL helper not found at $HELPER"; exit 1; } for b in unshare newuidmap newgidmap; do command -v "$b" >/dev/null 2>&1 || { echo " SKIP $b not installed"; exit 0; } done usr=$(id -un); u=$(id -u); g=$(id -g) uline=$(grep "^${usr}:" /etc/subuid 2>/dev/null | head -1) [[ -n "$uline" ]] || { echo " SKIP no subuid range for $usr"; exit 0; } sub="${uline#*:}"; sub="${sub%%:*}" CUID=$(( sub + 471 )) # a container uid, the way rootless docker maps one BASE=$(mktemp -d "${TMPDIR:-/tmp}/lp-userns-test-XXXXXX") || exit 1 trap 'rm -rf "$BASE"' EXIT echo "--- $usr ($u:$g), subuid base $sub, container uid $CUID ---" # Ask the helper to set the three ownerships a restore actually encounters. bash "$HELPER" bash -c ' : > "$1/app"; : > "$1/own"; : > "$1/croot" chown '"$CUID"':'"$g"' "$1/app" 2>/dev/null chown '"$u"':'"$g"' "$1/own" 2>/dev/null chown '"$sub"':'"$sub"' "$1/croot" 2>/dev/null exit 0' _ "$BASE" >/dev/null 2>&1 echo "--- container-owned app data (the grafana case) ---" chk "uid:gid" "$(stat -c '%u:%g' "$BASE/app")" "$CUID:$g" echo "--- LibrePortal's own files ---" # The caller's uid is spent on inner root, so this chown cannot succeed — and # does not need to: inner root IS the caller on the outside. chk "uid:gid" "$(stat -c '%u:%g' "$BASE/own")" "$u:$g" echo "--- container root ---" chk "uid:gid" "$(stat -c '%u:%g' "$BASE/croot")" "$sub:$sub" echo "--- exit status and stdout are passed through ---" out=$(bash "$HELPER" bash -c 'echo hello; exit 7' 2>/dev/null); rc=$? chk "stdout" "$out" "hello" chk "status" "$rc" "7" echo "" if (( fail )); then echo "FAILED"; exit 1; fi echo "All ownership-mapping checks passed."