SPS · log · files · refs · tags · releases · people · languages · GitHub · commit
bae70fd
pkin: adopt identical unowned files instead of treating them as hijacks.
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