diff --git a/test/shell.d/windows-vm-compose-test.sh b/test/shell.d/windows-vm-compose-test.sh index 64825526..189536f4 100644 --- a/test/shell.d/windows-vm-compose-test.sh +++ b/test/shell.d/windows-vm-compose-test.sh @@ -213,6 +213,65 @@ mv -T -- "$raced_shared" "$HOME/Windows" unset -f dc pass "a post-validation path swap cannot redirect Docker away from the pinned shared inode" +# Run the same attack as a genuinely concurrent process. A successful bring-up +# deliberately waits inside the Docker boundary until the attacker has replaced +# the familiar path with /, then verifies that the real bind anchor still names +# the caller-owned directory that was pinned before the race. +reset_case +prepare_user_mount_sources +touch "$HOME/Windows/safe-marker" +write 4G 2 64G concurrent pw UTC +resolve_caller +concurrent_shared_id=$(stat -Lc '%d:%i' "$HOME/Windows") +host_root_id=$(stat -Lc '%d:%i' /) +race_source="$HOME/Windows.race-source" +race_stop="$TMPDIR/stop-concurrent-race" +race_swaps="$TMPDIR/concurrent-race-swaps" +( + set +e + while [[ ! -e $race_stop ]]; do + if [[ -d $HOME/Windows && ! -L $HOME/Windows ]] && mv -T -- "$HOME/Windows" "$race_source" 2>/dev/null; then + ln -s / "$HOME/Windows" 2>/dev/null || true + printf x >>"$race_swaps" + sleep 0.002 + fi + if [[ -L $HOME/Windows ]]; then + rm -f -- "$HOME/Windows" + mv -T -- "$race_source" "$HOME/Windows" 2>/dev/null || true + sleep 0.005 + fi + done +) & +racer_pid=$! +concurrent_dc_calls=0 +dc() { + local attempt + [[ $1 == up && ${2:-} == -d ]] || return 1 + for ((attempt = 0; attempt < 20000; attempt++)); do + if [[ -L $HOME/Windows && $(readlink "$HOME/Windows" 2>/dev/null) == / ]]; then + break + fi + done + [[ -L $HOME/Windows && $(readlink "$HOME/Windows" 2>/dev/null) == / ]] || return 1 + ((concurrent_dc_calls++)) + [[ $(get_mount_source /shared) == "$EXPECTED_SHARED" ]] || return 1 + [[ $(stat -Lc '%d:%i' "$EXPECTED_SHARED") == "$concurrent_shared_id" ]] || return 1 + [[ $(stat -Lc '%d:%i' "$EXPECTED_SHARED") != "$host_root_id" ]] || return 1 + [[ -f $EXPECTED_SHARED/safe-marker ]] +} +for ((attempt = 0; attempt < 200; attempt++)); do + if __priv_up 2>/dev/null; then break; fi +done +touch "$race_stop" +wait "$racer_pid" +unset -f dc +if [[ -L $HOME/Windows ]]; then rm -f -- "$HOME/Windows"; fi +if [[ ! -e $HOME/Windows && -d $race_source ]]; then mv -T -- "$race_source" "$HOME/Windows"; fi +[[ -s $race_swaps ]] || fail "concurrent attacker never swapped the shared path" +((concurrent_dc_calls > 0)) || fail "concurrent race never reached Docker while the familiar path named host root" +[[ $(stat -Lc '%d:%i' "$EXPECTED_SHARED") == "$concurrent_shared_id" ]] || fail "concurrent race changed the protected shared inode" +pass "a concurrent home-path swap cannot redirect Docker away from the pinned shared inode" + # Same-inode sources fail before mounting and close both descriptors. reset_case same="$TMPDIR/same-source"