Splux Linux

source-based Linux using SPS

SPS · log · files · refs · tags · releases · people · languages · GitHub · commit

bae70fd


pkin: adopt identical unowned files instead of treating them as hijacks.

Date
Commitbae70fdc90928966d25e82af483d9962d21d5031
GitHubmirror

Parents

Stat

 DESIGN.md              |  6 ++++-
 bin/pkin               | 26 +++++++++++++++++++++
 tests/test_security.sh | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 92 insertions(+), 1 deletion(-)

Diff

diff --git a/DESIGN.md b/DESIGN.md
index 4ad8cbb..ba3671b 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -240,7 +240,11 @@ is reported as an orphan.
 A managed non-directory path has one SPS owner. `pkin` checks both the owner
 index and the target filesystem before writing a payload entry.
 
-An existing unowned file is a conflict. SPS does not silently claim it.
+An existing unowned file is a conflict unless it is already identical to the
+payload: the same symlink target, or a regular file with the same SHA-256
+digest. Identical paths are adopted so a live image can install `filesystem`
+over usr-merge links it already created. A different target or digest still
+fails.
 
 Directories are shared and are not stored in the exclusive owner index.
 Existing shared directories are not re-owned or chmodded just because another
diff --git a/bin/pkin b/bin/pkin
index 05ad2c4..070a118 100755
--- a/bin/pkin
+++ b/bin/pkin
@@ -503,9 +503,35 @@ fi
 LC_ALL=C sort -u "$sps_work/self-owned" -o "$sps_work/self-owned"
 comm -23 "$sps_work/owned" "$sps_work/self-owned" >"$sps_work/unowned"
 
+# Live images and other bootstraps often already contain usr-merge
+# symlinks and identical static files. Claiming the same bytes is not
+# a hijack; a different target or digest still is.
+sps_payload_matches_existing()
+{
+    sps_match_path=$1
+    sps_match_target=$2
+    sps_match_source=$sps_stage/$sps_match_path
+    if [ -L "$sps_match_source" ] && [ -L "$sps_match_target" ]; then
+        [ "$(readlink "$sps_match_source")" = "$(readlink "$sps_match_target")" ]
+        return $?
+    fi
+    if [ -f "$sps_match_source" ] && [ ! -L "$sps_match_source" ] &&
+       [ -f "$sps_match_target" ] && [ ! -L "$sps_match_target" ]
+    then
+        sps_match_new=$(sps_sha256 "$sps_match_source") || return 1
+        sps_match_old=$(sps_sha256 "$sps_match_target") || return 1
+        [ "$sps_match_new" = "$sps_match_old" ]
+        return $?
+    fi
+    return 1
+}
+
 while IFS= read -r sps_path || [ -n "$sps_path" ]; do
     sps_target_path "$sps_path"
     if sps_exists "$sps_target"; then
+        if sps_payload_matches_existing "$sps_path" "$sps_target"; then
+            continue
+        fi
         sps_fail 7 "/$sps_path exists and is not owned by an installed package"
     fi
 done <"$sps_work/unowned"
diff --git a/tests/test_security.sh b/tests/test_security.sh
index ca424fe..2f89df7 100755
--- a/tests/test_security.sh
+++ b/tests/test_security.sh
@@ -165,6 +165,67 @@ printf '%s\n' administrator >"$root/usr/bin/occupied"
 expect_status 7 "$project_dir/bin/pkin" "$archive"
 [ "$(cat "$root/usr/bin/occupied")" = administrator ] || fail 'unowned collision was overwritten'
 
+# Identical unowned bytes are adopted. Live /bin -> usr/bin must not block
+# filesystem, but a different symlink target is still a conflict.
+fresh_root
+stage=$tmp/stage-identical-link
+mkdir -p "$stage/.SPS" "$stage/usr/bin" "$root/usr/bin"
+ln -s usr/bin "$stage/bin"
+ln -s usr/bin "$root/bin"
+printf '%s\n' same >"$stage/usr/bin/tool"
+printf '%s\n' same >"$root/usr/bin/tool"
+{
+    printf 'format\t1\n'
+    printf 'name\tidenticallink\nversion\t1.0\nrelease\t1\narch\tany\n'
+} >"$stage/.SPS/meta"
+: >"$stage/.SPS/files.tmp"
+(CDPATH= cd "$stage" && find . ! -name . ! -path './.SPS' ! -path './.SPS/*' -print) |
+sed 's#^\./##' | while IFS= read -r package_entry; do
+    if [ -d "$stage/$package_entry" ] && [ ! -L "$stage/$package_entry" ]; then
+        printf '%s/\n' "$package_entry"
+    else
+        printf '%s\n' "$package_entry"
+    fi
+done >"$stage/.SPS/files.tmp"
+LC_ALL=C sort "$stage/.SPS/files.tmp" >"$stage/.SPS/files"
+rm -f "$stage/.SPS/files.tmp"
+printf 'sha256\t%s\tusr/bin/tool\n' \
+    "$(sha256sum "$stage/usr/bin/tool" | awk '{ print $1 }')" \
+    >"$stage/.SPS/hashes"
+tar -C "$stage" -cf "$tmp/identicallink.pkg.tar" .
+run_sps "$project_dir/bin/pkin" "$tmp/identicallink.pkg.tar" >/dev/null ||
+    fail 'identical unowned symlink/file should be adopted'
+[ -L "$root/bin" ] || fail 'adopted usr-merge /bin was lost'
+[ "$(readlink "$root/bin")" = usr/bin ] || fail 'adopted /bin target changed'
+
+fresh_root
+stage=$tmp/stage-different-link
+mkdir -p "$stage/.SPS" "$stage/usr/bin" "$root/usr/sbin"
+ln -s usr/bin "$stage/bin"
+ln -s usr/sbin "$root/bin"
+printf '%s\n' same >"$stage/usr/bin/tool"
+{
+    printf 'format\t1\n'
+    printf 'name\tdifferentlink\nversion\t1.0\nrelease\t1\narch\tany\n'
+} >"$stage/.SPS/meta"
+: >"$stage/.SPS/files.tmp"
+(CDPATH= cd "$stage" && find . ! -name . ! -path './.SPS' ! -path './.SPS/*' -print) |
+sed 's#^\./##' | while IFS= read -r package_entry; do
+    if [ -d "$stage/$package_entry" ] && [ ! -L "$stage/$package_entry" ]; then
+        printf '%s/\n' "$package_entry"
+    else
+        printf '%s\n' "$package_entry"
+    fi
+done >"$stage/.SPS/files.tmp"
+LC_ALL=C sort "$stage/.SPS/files.tmp" >"$stage/.SPS/files"
+rm -f "$stage/.SPS/files.tmp"
+printf 'sha256\t%s\tusr/bin/tool\n' \
+    "$(sha256sum "$stage/usr/bin/tool" | awk '{ print $1 }')" \
+    >"$stage/.SPS/hashes"
+tar -C "$stage" -cf "$tmp/differentlink.pkg.tar" .
+expect_status 7 "$project_dir/bin/pkin" "$tmp/differentlink.pkg.tar"
+[ "$(readlink "$root/bin")" = usr/sbin ] || fail 'different symlink target was replaced'
+
 fresh_root
 archive=$(make_basic redirected 1.0)
 outside=$tmp/outside-target