3"""Adversarial permission and race selftests for the pinned uv bootstrap."""
5from __future__
import annotations
18from collections.abc
import Callable
19from functools
import partial
20from pathlib
import Path
21from typing
import Any, cast
22from unittest
import mock
24b = cast(Any, globals()[
"bootstrap"])
25ACTUAL_UV_RUN_INVOCATION = 2
26EXPECTED_CHILD_STATUS = 37
28APPLY_REQUIRED_STATUS = 2
29EXPECTED_PORTABLE_READER_OPENS = 3
33 root: Path, populate: bool =
True, binary: bytes = b
"verified-uv"
34) -> tuple[Path, Path, Path, str, bytes]:
35 """Create one release-shaped mode fixture and return its exact authorities."""
36 key = b.asset_key(platform.system(), platform.machine())
37 asset_name = b.expected_asset_name(key)
38 payload = b.synthetic_archive(asset_name, binary)
41 "repository":
"astral-sh/uv",
43 "assets": {key: {
"name": asset_name,
"sha256": hashlib.sha256(payload).hexdigest()}},
45 manifest_path = root /
"manifest.json"
46 manifest_path.write_text(json.dumps(manifest), encoding=
"ascii")
47 destination = b.cache_destination(root,
"0.0.0", asset_name)
48 archive = destination.parent / asset_name
50 destination.parent.mkdir(parents=
True)
51 archive.write_bytes(payload)
52 destination.write_bytes(binary)
53 archive.chmod(b.PRIVATE_ARCHIVE_MODE)
54 destination.chmod(b.PRIVATE_EXECUTABLE_MODE)
55 return manifest_path, archive, destination, asset_name, payload
58def expect_modes(archive: Path, destination: Path, archive_mode: int, binary_mode: int) ->
None:
59 """Require exact fixture modes after one positive or negative action."""
60 if stat.S_IMODE(archive.stat().st_mode) != archive_mode:
61 b.fail(f
"selftest: cached archive mode is not {archive_mode:04o}")
62 if stat.S_IMODE(destination.stat().st_mode) != binary_mode:
63 b.fail(f
"selftest: cached executable mode is not {binary_mode:04o}")
66def expect_exec_failure(action: Callable[[], object], label: str) ->
None:
67 """Require one execution-boundary action to fail closed."""
70 except (OSError, b.bootstrap_uv_exec.UvExecError):
72 if isinstance(result, int):
74 b.fail(f
"selftest: uv execution boundary {label} passed unexpectedly")
77def owned_stat_wrapper(
78 real_stat: Callable[..., os.stat_result], alias: str, root_descriptor: int, owner_uid: int
79) -> Callable[..., os.stat_result]:
80 """Give a Darwin alias an explicit owner independent of the test process."""
83 candidate: str | Path,
85 dir_fd: int |
None =
None,
86 follow_symlinks: bool =
True,
88 state = real_stat(candidate, dir_fd=dir_fd, follow_symlinks=follow_symlinks)
89 if str(candidate) == alias
and dir_fd == root_descriptor
and not follow_symlinks:
92 return os.stat_result(fields)
98def open_simulated_darwin_parent(
102 scenario: str =
"valid",
104 """Run the production parent walker against one synthetic system root."""
105 platform_name =
"linux" if scenario ==
"linux" else "darwin"
106 root_owned = scenario !=
"owner"
107 redirect_alias_to =
"attacker/var" if scenario ==
"inode" else None
108 reject_root = scenario ==
"root"
109 directory_flags = os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC | os.O_DIRECTORY
112 root_descriptor = real_open(root, directory_flags)
115 candidate: str | Path,
119 dir_fd: int |
None =
None,
122 str(candidate) == alias
and dir_fd == root_descriptor
and flags & os.O_NOFOLLOW == 0
124 if is_alias_follow
and redirect_alias_to
is not None:
125 return real_open(redirect_alias_to, flags, mode, dir_fd=root_descriptor)
126 return real_open(candidate, flags, mode, dir_fd=dir_fd)
128 stat_hook = owned_stat_wrapper(
132 0
if root_owned
else 1,
134 root_error = b.bootstrap_uv_exec.UvExecError(
"simulated untrusted system root")
135 trust_hook = mock.Mock(side_effect=root_error)
if reject_root
else mock.Mock()
137 mock.patch.object(os,
"open", side_effect=wrapped_open),
138 mock.patch.object(os,
"stat", side_effect=stat_hook),
139 mock.patch.object(b.bootstrap_uv_exec,
"_require_trusted_system_root", trust_hook),
141 return b.bootstrap_uv_exec.open_parent_components(
146 platform_name=platform_name,
150def darwin_root_alias_acceptance_selftest() -> None:
151 """Accept only the two fixed Darwin aliases and the physical spelling."""
152 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-darwin-alias-ok-")
as raw:
154 for alias
in (
"var",
"tmp"):
155 physical = root /
"private" / alias /
"folders"
156 physical.mkdir(parents=
True)
157 (root / alias).symlink_to(Path(
"private") / alias, target_is_directory=
True)
158 descriptor = open_simulated_darwin_parent(root, alias)
160 state = os.fstat(descriptor)
161 if (state.st_dev, state.st_ino) != (
162 physical.stat().st_dev,
163 physical.stat().st_ino,
165 b.fail(f
"selftest: Darwin /{alias} alias opened the wrong directory")
168 flags = os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC | os.O_DIRECTORY
169 root_descriptor = os.open(root, flags)
170 descriptor = b.bootstrap_uv_exec.open_parent_components(
172 (
"private",
"var",
"folders"),
175 platform_name=
"darwin",
180def darwin_root_alias_rejection_selftest() -> None:
181 """Reject hostile targets, owners, identities, roots, and later symlinks."""
182 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-darwin-alias-bad-")
as raw:
184 (root /
"private" /
"var" /
"folders").mkdir(parents=
True)
185 (root /
"attacker" /
"var" /
"folders").mkdir(parents=
True)
186 (root /
"var").symlink_to(
"attacker/var", target_is_directory=
True)
187 expect_exec_failure(partial(open_simulated_darwin_parent, root,
"var"),
"alias target")
188 (root /
"var").unlink()
189 (root /
"var").symlink_to(
"private/var", target_is_directory=
True)
191 partial(open_simulated_darwin_parent, root,
"var", scenario=
"owner"),
195 partial(open_simulated_darwin_parent, root,
"var", scenario=
"inode"),
199 partial(open_simulated_darwin_parent, root,
"var", scenario=
"root"),
203 partial(open_simulated_darwin_parent, root,
"var", scenario=
"linux"),
204 "accepted outside Darwin",
206 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-darwin-later-link-")
as raw:
208 physical = root /
"private" /
"tmp"
209 (physical /
"real-folders").mkdir(parents=
True)
210 (physical /
"folders").symlink_to(
"real-folders", target_is_directory=
True)
211 (root /
"tmp").symlink_to(
"private/tmp", target_is_directory=
True)
212 expect_exec_failure(partial(open_simulated_darwin_parent, root,
"tmp"),
"alias later link")
215def write_metadata(path: Path) -> tuple[int, int, int, int]:
216 """Return file metadata that can change only through a write-like operation."""
218 return stat.S_IMODE(state.st_mode), state.st_size, state.st_mtime_ns, state.st_ctime_ns
221def cache_mode_convergence_selftest() -> None:
222 """Prove fresh and retained POSIX caches converge after exact authentication."""
223 probe = subprocess.CompletedProcess([
"uv",
"--version"], 0,
"uv 0.0.0\n",
"")
224 for populate
in (
False,
True):
225 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-mode-test-")
as raw:
227 manifest, archive, destination, _, payload = mode_fixture(root, populate)
229 mock.patch.object(subprocess,
"run", return_value=probe),
230 mock.patch.object(b,
"download_payload", return_value=payload),
232 b.ensure_uv(manifest, root)
233 expect_modes(archive, destination, *b.EXPECTED_PUBLIC_MODES)
236def cache_mode_authentication_selftest() -> None:
237 """Prove digest and byte mutations cannot reach either fchmod call."""
238 for target
in (
"archive",
"binary"):
239 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-auth-mode-")
as raw:
241 manifest, archive, destination, _, payload = mode_fixture(root)
242 if target ==
"archive":
243 mutated = bytearray(payload)
245 archive.write_bytes(mutated)
247 destination.write_bytes(b
"other-uv")
248 b.expect_bootstrap_error(
249 lambda manifest=manifest, root=root: b.ensure_uv(manifest, root),
250 f
"tampered cached {target} permission repair",
255 b.PRIVATE_ARCHIVE_MODE,
256 b.PRIVATE_EXECUTABLE_MODE,
260def cache_open_flags_selftest() -> None:
261 """Prove parent and final FD opens require every POSIX safety flag."""
262 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-open-flags-")
as raw:
263 path = Path(raw) /
"artifact"
264 path.write_bytes(b
"verified")
266 seen: list[tuple[str, int, int |
None, bool]] = []
269 candidate: str | Path,
273 dir_fd: int |
None =
None,
275 parent_is_root =
False
276 if dir_fd
is not None:
277 parent_state = os.fstat(dir_fd)
278 root_state = Path(
"/").stat(follow_symlinks=
False)
279 parent_is_root = (parent_state.st_dev, parent_state.st_ino) == (
283 seen.append((str(candidate), flags, dir_fd, parent_is_root))
284 return real_open(candidate, flags, mode, dir_fd=dir_fd)
286 with mock.patch.object(os,
"open", side_effect=record_open):
287 descriptor = b.open_cache_fd(path)
289 final = [item
for item
in seen
if item[0] == path.name
and item[2]
is not None]
290 final_required = os.O_NOFOLLOW | os.O_CLOEXEC | os.O_NONBLOCK
291 parent_required = os.O_NOFOLLOW | os.O_CLOEXEC | os.O_DIRECTORY
292 parents = [item
for item
in seen
if item
not in final]
293 if len(final) != 1
or not parents:
294 b.fail(
"selftest: cache parent/final FD open structure changed")
295 if final[0][1] & final_required != final_required:
296 b.fail(
"selftest: exact cache FD open omitted a POSIX safety flag")
297 unsafe_parents = [item
for item
in parents
if item[1] & parent_required != parent_required]
298 expected_alias_follows = int(
299 (sys.platform, path.parts[1])
in {(
"darwin",
"tmp"), (
"darwin",
"var")}
301 if len(unsafe_parents) != expected_alias_follows:
302 b.fail(
"selftest: cache parent FD open omitted a POSIX safety flag")
303 alias_required = os.O_CLOEXEC | os.O_DIRECTORY
304 for name, flags, _dir_fd, parent_is_root
in unsafe_parents:
305 if (name, parent_is_root)
not in {(
"tmp",
True), (
"var",
True)}:
306 b.fail(
"selftest: non-root cache parent followed a symlink")
307 if flags & alias_required != alias_required
or flags & os.O_NOFOLLOW:
308 b.fail(
"selftest: Darwin root alias open used unsafe flags")
309 for name
in (
"O_NOFOLLOW",
"O_CLOEXEC",
"O_NONBLOCK",
"O_DIRECTORY"):
310 with mock.patch.object(os, name,
None):
311 b.expect_bootstrap_error(
312 partial(b.open_cache_fd, path),
313 f
"missing {name} support",
317def cache_parent_symlink_selftest() -> None:
318 """Prove a symlink in a cache parent cannot redirect the final open."""
319 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-parent-symlink-")
as raw:
320 root = Path(raw).resolve()
323 artifact = real /
"artifact"
324 artifact.write_bytes(b
"verified")
326 link.symlink_to(real, target_is_directory=
True)
327 b.expect_bootstrap_error(
328 partial(b.open_cache_fd, link / artifact.name),
329 "cache parent symlink",
330 "cannot open cached uv parent",
334def cache_parent_swap_selftest() -> None:
335 """Prove a parent rename plus same-inode symlink cannot pass revalidation."""
336 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-parent-swap-")
as raw:
337 root = Path(raw).resolve()
338 _manifest, archive, destination, asset_name, payload = mode_fixture(root)
339 original_parent = destination.parent
340 displaced = root /
"displaced-authenticated-parent"
344 def swap_parent_then_open(
345 candidate: str | Path,
349 dir_fd: int |
None =
None,
352 is_final = str(candidate) == destination.name
and dir_fd
is not None
353 if is_final
and flags & os.O_NONBLOCK
and not swapped:
355 original_parent.rename(displaced)
356 original_parent.symlink_to(displaced, target_is_directory=
True)
357 return real_open(candidate, flags, mode, dir_fd=dir_fd)
359 digest = hashlib.sha256(payload).hexdigest()
360 with mock.patch.object(os,
"open", side_effect=swap_parent_then_open):
361 b.expect_bootstrap_error(
363 b.normalize_cached_modes,
369 "cache parent rename/symlink swap",
370 "moved during permission repair",
373 displaced / archive.name,
374 displaced / destination.name,
375 b.PUBLIC_ARCHIVE_MODE,
376 b.PUBLIC_EXECUTABLE_MODE,
380def cache_atomic_parent_swap_selftest() -> None:
381 """Prove apply writes stay in a held parent when its path is redirected."""
382 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-write-swap-")
as raw:
383 root = Path(raw).resolve()
384 manifest, archive, destination, _asset_name, payload = mode_fixture(root, populate=
False)
385 external = root /
"external-parent"
387 original_parent = destination.parent
388 displaced = root /
"displaced-write-parent"
389 real_replace = os.replace
392 def swap_parent_then_replace(
396 src_dir_fd: int |
None =
None,
397 dst_dir_fd: int |
None =
None,
400 readiness = (swapped, src_dir_fd
is None, dst_dir_fd
is None)
401 if readiness == (
False,
False,
False):
403 original_parent.rename(displaced)
404 original_parent.symlink_to(external, target_is_directory=
True)
408 src_dir_fd=src_dir_fd,
409 dst_dir_fd=dst_dir_fd,
413 mock.patch.object(b,
"download_payload", return_value=payload),
414 mock.patch.object(os,
"replace", side_effect=swap_parent_then_replace),
416 b.expect_bootstrap_error(
417 partial(b.ensure_uv, manifest, root),
418 "atomic cache write parent swap",
419 "cannot open cached uv parent",
421 if list(external.iterdir()):
422 b.fail(
"selftest: redirected parent received a privileged uv cache write")
423 if (displaced / archive.name).read_bytes() != payload:
424 b.fail(
"selftest: held cache parent did not receive exact archive bytes")
427def cache_nonregular_selftest() -> None:
428 """Prove every non-regular cache node is rejected without blocking."""
429 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-node-mode-")
as raw:
431 regular = root /
"regular"
432 regular.write_bytes(b
"verified")
433 hardlink = root /
"hardlink"
434 hardlink.hardlink_to(regular)
435 directory = root /
"directory"
438 os.mkfifo(fifo, 0o600)
439 socket_path = root /
"socket"
440 listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
441 listener.bind(str(socket_path))
443 def timeout(_signum: int, _frame: object) ->
None:
444 message =
"non-regular cache open blocked"
445 raise RuntimeError(message)
447 previous = signal.signal(signal.SIGALRM, timeout)
449 for path
in (regular, hardlink, directory, fifo, socket_path):
451 b.expect_bootstrap_error(
452 partial(b.open_cache_fd, path),
453 f
"non-regular cache node {path.name}",
458 signal.signal(signal.SIGALRM, previous)
462def expect_concurrent_read_rejected(path: Path, replacement: bytes, label: str) ->
None:
463 """Require one concurrent inode rewrite to invalidate a bounded read."""
464 descriptor = b.open_cache_fd(path)
465 real_fdopen = os.fdopen
467 def rewrite_then_open(duplicate: int, *args: object, **kwargs: object) -> object:
469 path.write_bytes(replacement)
470 if len(replacement) == before.st_size:
471 os.utime(path, ns=(before.st_atime_ns, before.st_mtime_ns + 1))
472 return real_fdopen(duplicate, *args, **kwargs)
475 with mock.patch.object(os,
"fdopen", side_effect=rewrite_then_open):
476 b.expect_bootstrap_error(
477 partial(b.read_stable_fd, descriptor, path, 8),
479 "changed while authenticating",
485def cache_stable_read_selftest() -> None:
486 """Prove bounded reads reject excess size and two concurrent rewrites."""
487 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-stable-read-")
as raw:
488 path = Path(raw) /
"artifact"
489 path.write_bytes(b
"12345678")
490 descriptor = b.open_cache_fd(path)
492 payload, _ = b.read_stable_fd(descriptor, path, 8)
493 if payload != b
"12345678":
494 b.fail(
"selftest: maximum-sized cache read changed bytes")
497 path.write_bytes(b
"123456789")
498 descriptor = b.open_cache_fd(path)
500 b.expect_bootstrap_error(
501 partial(b.read_stable_fd, descriptor, path, 8),
502 "oversized cached artifact",
507 path.write_bytes(b
"12345678")
508 expect_concurrent_read_rejected(path, b
"x",
"concurrently truncated cached artifact")
509 path.write_bytes(b
"12345678")
510 expect_concurrent_read_rejected(
511 path, b
"ABCDEFGH",
"concurrent same-size cached artifact rewrite"
515def cache_verification_fifo_race_selftest() -> None:
516 """Prove the read-only verifier cannot block on a raced FIFO."""
517 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-verify-fifo-")
as raw:
519 manifest, archive, destination, _, _ = mode_fixture(root)
520 archive.chmod(b.PUBLIC_ARCHIVE_MODE)
521 destination.chmod(b.PUBLIC_EXECUTABLE_MODE)
522 real_open = b.open_cache_fd
525 def swap_then_open(path: Path) -> int:
527 if path == archive
and not swapped:
530 os.mkfifo(archive, 0o600)
531 return real_open(path)
533 with mock.patch.object(b,
"open_cache_fd", side_effect=swap_then_open):
534 b.expect_bootstrap_error(
535 partial(b.verify_cached_uv, manifest, root),
536 "read-only verification FIFO race",
537 "not one single-link regular file",
541def cache_exact_fd_execution_selftest() -> None:
542 """Prove actual uv work executes authenticated bytes, not a replacement path."""
543 good = b
'#!/bin/sh\nif [ "${1:-}" = --version ]; then echo uv 0.0.0; fi\n'
544 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-exact-exec-")
as raw:
546 manifest, archive, destination, _, _ = mode_fixture(root, binary=good)
547 archive.chmod(b.PUBLIC_ARCHIVE_MODE)
548 destination.chmod(b.PUBLIC_EXECUTABLE_MODE)
549 victim = root /
"unauthenticated-executed"
550 real_run = subprocess.run
553 def swap_then_run(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
556 if calls == ACTUAL_UV_RUN_INVOCATION:
557 replacement = destination.with_name(
"replacement")
558 displaced = destination.with_name(
"displaced-authenticated")
559 replacement.write_text(
560 f
"#!/bin/sh\ntouch {victim}\necho uv 0.0.0\n", encoding=
"ascii"
562 replacement.chmod(b.PUBLIC_EXECUTABLE_MODE)
563 destination.rename(displaced)
564 replacement.replace(destination)
565 return real_run(argv, **kwargs)
567 with mock.patch.object(subprocess,
"run", side_effect=swap_then_run):
569 b.run_cached_uv(manifest, root, [
"work"], ensure=
False)
570 except (b.CacheMetadataChangedError, b.CachePathBindingError):
572 except b.BootstrapError:
573 b.fail(
"selftest: pathname replacement returned unrelated error class")
575 b.fail(
"selftest: pathname replacement was not rejected")
577 b.fail(
"selftest: version probe executed unauthenticated replacement bytes")
580def cache_same_inode_execution_selftest() -> None:
581 """Prove a post-auth cache-inode write cannot control actual uv work."""
582 good = b
'#!/bin/sh\nif [ "${1:-}" = --version ]; then echo uv 0.0.0; fi\n'
583 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-inode-exec-")
as raw:
585 manifest, archive, destination, _, _ = mode_fixture(root, binary=good)
586 archive.chmod(b.PUBLIC_ARCHIVE_MODE)
587 destination.chmod(b.PUBLIC_EXECUTABLE_MODE)
588 victim = root /
"unauthenticated-inode-executed"
589 malicious = f
"#!/bin/sh\ntouch {victim}\necho uv 0.0.0\n".encode(
"ascii")
590 real_run = subprocess.run
593 def mutate_inode_then_run(
594 argv: list[str], **kwargs: object
595 ) -> subprocess.CompletedProcess[str]:
598 if calls == ACTUAL_UV_RUN_INVOCATION:
599 with destination.open(
"r+b")
as target:
600 target.write(malicious)
603 os.fsync(target.fileno())
604 return real_run(argv, **kwargs)
606 with mock.patch.object(subprocess,
"run", side_effect=mutate_inode_then_run):
607 b.expect_bootstrap_error(
608 partial(b.run_cached_uv, manifest, root, [
"work"], ensure=
False),
609 "post-auth same-inode cache mutation",
610 "changed after authenticating",
613 b.fail(
"selftest: version probe executed post-auth cache inode bytes")
616def cache_run_exit_status_selftest() -> None:
617 """Prove the public --run mode returns the exact immutable child status."""
619 b
'#!/bin/sh\nif [ "${1:-}" = --version ]; then echo uv 0.0.0; exit 0; fi\n'
620 b
'if [ "${1:-}" = exit ]; then exit "$2"; fi\n'
622 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-run-status-")
as raw:
624 manifest, archive, destination, _, _ = mode_fixture(root, binary=binary)
625 archive.chmod(b.PUBLIC_ARCHIVE_MODE)
626 destination.chmod(b.PUBLIC_EXECUTABLE_MODE)
627 for execution_mode
in (
"--run",
"--ensure-and-run"):
632 str(Path(b.__file__)),
639 str(EXPECTED_CHILD_STATUS),
641 process = os.posix_spawn(argv[0], argv, os.environ.copy())
642 waited, status = os.waitpid(process, 0)
643 if waited != process
or os.waitstatus_to_exitcode(status) != EXPECTED_CHILD_STATUS:
644 b.fail(f
"selftest: {execution_mode} did not preserve the uv child status")
647def cache_run_signal_status_selftest() -> None:
648 """Prove public run modes reproduce an immutable child's terminating signal."""
650 b
'#!/bin/sh\nif [ "${1:-}" = --version ]; then echo uv 0.0.0; exit 0; fi\n'
651 b
'if [ "${1:-}" = signal ]; then kill -TERM $$; fi\n'
653 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-run-signal-")
as raw:
655 manifest, archive, destination, _, _ = mode_fixture(root, binary=binary)
656 archive.chmod(b.PUBLIC_ARCHIVE_MODE)
657 destination.chmod(b.PUBLIC_EXECUTABLE_MODE)
658 for execution_mode
in (
"--run",
"--ensure-and-run"):
663 str(Path(b.__file__)),
671 process = os.posix_spawn(argv[0], argv, os.environ.copy())
672 waited, status = os.waitpid(process, 0)
673 if waited != process
or not os.WIFSIGNALED(status):
674 b.fail(f
"selftest: {execution_mode} did not propagate the uv child signal")
675 if os.WTERMSIG(status) != signal.SIGTERM:
676 b.fail(f
"selftest: {execution_mode} changed the uv child signal")
679def portable_readonly_fd_selftest() -> None:
680 """Prove the non-Linux fallback yields one exact private executable."""
681 binary = b
"#!/bin/sh\nexit 0\n"
682 controls = importlib.import_module(
"fcntl")
684 with b.bootstrap_uv_exec.portable_named_exec_snapshot(binary)
as snapshot:
685 descriptor, executable = snapshot
686 access_mode = controls.fcntl(descriptor, controls.F_GETFL) & os.O_ACCMODE
687 if access_mode != os.O_RDONLY:
688 b.fail(
"selftest: portable uv execution descriptor retained write access")
689 if os.fstat(descriptor).st_nlink != 1
or not Path(executable).is_file():
690 b.fail(
"selftest: portable uv execution name lost its bound descriptor")
691 if os.pread(descriptor, len(binary) + 1, 0) != binary:
692 b.fail(
"selftest: portable uv execution descriptor changed bytes")
694 os.write(descriptor, b
"x")
698 b.fail(
"selftest: portable uv execution descriptor accepted a write")
699 process = os.posix_spawn(executable, [executable], os.environ.copy())
700 waited, status = os.waitpid(process, 0)
701 if waited != process
or os.waitstatus_to_exitcode(status) != 0:
702 b.fail(
"selftest: portable uv execution path did not execute exact bytes")
703 if Path(executable).exists():
704 b.fail(
"selftest: portable uv execution name survived cleanup")
707def run_portable_snapshot(binary: bytes) ->
None:
708 """Enter and leave one portable snapshot for negative attack tests."""
709 with b.bootstrap_uv_exec.portable_named_exec_snapshot(binary):
713def portable_snapshot_flags_selftest() -> None:
714 """Prove the named portable snapshot is reopened with every safety flag."""
716 reader_flags: list[int] = []
719 candidate: str | Path,
723 dir_fd: int |
None =
None,
725 is_reader = str(candidate).startswith(
".ra8-uv-")
and flags & os.O_CREAT == 0
727 reader_flags.append(flags)
728 return real_open(candidate, flags, mode, dir_fd=dir_fd)
730 with mock.patch.object(os,
"open", side_effect=record_open):
731 run_portable_snapshot(b
"verified-uv")
732 required = os.O_NONBLOCK | os.O_CLOEXEC | os.O_NOFOLLOW
733 if len(reader_flags) != EXPECTED_PORTABLE_READER_OPENS:
734 b.fail(
"selftest: portable uv reader authentication count changed")
735 if any(flags & required != required
for flags
in reader_flags):
736 b.fail(
"selftest: portable uv reader omitted a safety flag")
737 if any(flags & os.O_ACCMODE != os.O_RDONLY
for flags
in reader_flags):
738 b.fail(
"selftest: portable uv reader was not opened read-only")
741def portable_snapshot_path_attack_selftest(attack: str) ->
None:
742 """Reject one deterministic mutation between portable write and reopen."""
743 binary = b
"verified-uv"
744 replacement = b
"untrusted!!"
749 candidate: str | Path,
753 dir_fd: int |
None =
None,
756 is_reader = str(candidate).startswith(
".ra8-uv-")
and flags & os.O_CREAT == 0
757 if not is_reader
or attacked
or dir_fd
is None:
758 return real_open(candidate, flags, mode, dir_fd=dir_fd)
760 if attack
in {
"replace",
"symlink"}:
761 os.unlink(candidate, dir_fd=dir_fd)
762 if attack
in {
"replace",
"descriptor",
"symlink"}:
764 ".ra8-uv-victim", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o500, dir_fd=dir_fd
766 os.write(victim, replacement)
768 if attack ==
"replace":
769 os.rename(
".ra8-uv-victim", candidate, src_dir_fd=dir_fd, dst_dir_fd=dir_fd)
770 elif attack ==
"symlink":
771 os.symlink(
".ra8-uv-victim", candidate, dir_fd=dir_fd)
772 elif attack ==
"hardlink":
773 os.link(candidate,
".ra8-uv-extra", src_dir_fd=dir_fd, dst_dir_fd=dir_fd)
774 elif attack ==
"overwrite":
775 os.chmod(candidate, 0o700, dir_fd=dir_fd, follow_symlinks=
False)
776 writer = real_open(candidate, os.O_WRONLY | os.O_NOFOLLOW, dir_fd=dir_fd)
777 os.write(writer, replacement)
779 os.chmod(candidate, 0o500, dir_fd=dir_fd, follow_symlinks=
False)
780 elif attack ==
"descriptor":
781 return real_open(
".ra8-uv-victim", flags, dir_fd=dir_fd)
782 return real_open(candidate, flags, mode, dir_fd=dir_fd)
784 with mock.patch.object(os,
"open", side_effect=attack_open):
786 partial(run_portable_snapshot, binary),
787 f
"portable {attack} attack",
790 b.fail(f
"selftest: portable {attack} attack did not reach the reader open")
793def portable_snapshot_unlink_failure_selftest() -> None:
794 """Refuse to return a portable descriptor when its name cannot be removed."""
795 real_unlink = os.unlink
798 def reject_unlink(candidate: str | Path, *, dir_fd: int |
None =
None) ->
None:
800 if str(candidate).startswith(
".ra8-uv-")
and not attacked:
802 message =
"simulated unlink denial"
803 raise PermissionError(message)
804 real_unlink(candidate, dir_fd=dir_fd)
806 with mock.patch.object(os,
"unlink", side_effect=reject_unlink):
808 partial(run_portable_snapshot, b
"verified-uv"),
809 "portable unlink failure",
812 b.fail(
"selftest: portable unlink attack did not reach the unlink boundary")
815def bootstrap_run_status(manifest: Path, root: Path) -> int:
816 """Return the real public --run status for one cache fixture."""
821 str(Path(b.__file__)),
829 process = os.posix_spawn(argv[0], argv, os.environ.copy())
830 waited, status = os.waitpid(process, 0)
831 if waited != process:
832 b.fail(
"selftest: bootstrap status child identity changed")
833 return os.waitstatus_to_exitcode(status)
836def cache_status_contract_selftest() -> None:
837 """Prove repairable drift and authenticated-content failure use distinct types."""
838 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-status-mode-")
as raw:
840 manifest, archive, _destination, _, _payload = mode_fixture(root)
842 b.verify_cached_uv(manifest, root)
843 except b.CacheApplyRequiredError:
846 b.fail(
"selftest: repairable cache drift lacks its exact exception contract")
847 if bootstrap_run_status(manifest, root) != APPLY_REQUIRED_STATUS:
848 b.fail(
"selftest: repairable --run drift did not return status 2")
849 archive.write_bytes(b
"tampered")
851 b.verify_cached_uv(manifest, root)
852 except b.CacheApplyRequiredError:
853 b.fail(
"selftest: cache authentication failure was classified as drift")
854 except b.BootstrapError
as error:
855 if "SHA-256 mismatch" not in str(error):
856 b.fail(
"selftest: cache authentication failure returned the wrong error")
858 b.fail(
"selftest: cache authentication failure passed")
859 if bootstrap_run_status(manifest, root) != AUTH_ERROR_STATUS:
860 b.fail(
"selftest: authenticated --run failure did not return status 1")
862 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-missing-mode-")
as raw:
864 manifest, _archive, _destination, _, _ = mode_fixture(root, populate=
False)
866 b.verify_cached_uv(manifest, root)
867 except b.CacheApplyRequiredError:
870 b.fail(
"selftest: missing cache was not classified as repairable drift")
871 if bootstrap_run_status(manifest, root) != APPLY_REQUIRED_STATUS:
872 b.fail(
"selftest: missing-cache --run drift did not return status 2")
875def cache_mode_path_attack_selftest(
876 target_name: str, replacement_kind: str, preexisting: bool
878 """Prove symlink and moved-path attacks cannot redirect authenticated chmod."""
879 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-path-mode-")
as raw:
881 _, archive, destination, asset_name, payload = mode_fixture(root)
882 target = archive
if target_name ==
"archive" else destination
883 mode = b.PRIVATE_ARCHIVE_MODE
if target_name ==
"archive" else b.PRIVATE_EXECUTABLE_MODE
884 moved = root / f
"opened-{target_name}"
885 replacement = root /
"replacement"
886 replacement.write_bytes(target.read_bytes())
887 replacement.chmod(mode)
889 def replace_target() -> None:
890 """Replace the selected cache path without changing replacement bytes."""
892 if replacement_kind ==
"symlink":
893 target.symlink_to(replacement)
895 replacement.rename(target)
897 real_fchmod = os.fchmod
900 def swap_then_fchmod(descriptor: int, requested_mode: int) ->
None:
902 if calls == 0
and not preexisting:
905 real_fchmod(descriptor, requested_mode)
910 "cannot open cached uv artifact" if preexisting
else "moved during permission repair"
913 mock.patch.object(os,
"fchmod", side_effect=swap_then_fchmod),
914 mock.patch.object(os,
"chmod", side_effect=AssertionError(
"path-based chmod")),
916 b.expect_bootstrap_error(
918 b.normalize_cached_modes,
922 hashlib.sha256(payload).hexdigest(),
924 f
"{target_name} {replacement_kind} path attack",
927 if stat.S_IMODE(target.stat().st_mode) != mode:
928 b.fail(f
"selftest: {target_name} {replacement_kind} replacement received chmod")
931def cache_mode_readonly_and_windows_selftest() -> None:
932 """Prove audits never write and Windows never asserts POSIX permission bits."""
933 with tempfile.TemporaryDirectory(prefix=
"ra8-uv-readonly-mode-")
as raw:
935 manifest, archive, destination, _, _ = mode_fixture(root)
936 probe = subprocess.CompletedProcess([
"uv",
"--version"], 0,
"uv 0.0.0\n",
"")
937 before = (write_metadata(archive), write_metadata(destination))
938 with mock.patch.object(subprocess,
"run", return_value=probe):
939 if os.name ==
"posix":
940 b.expect_bootstrap_error(
941 lambda: b.verify_cached_uv(manifest, root),
942 "read-only audit detects mode drift",
943 "permissions require an apply",
946 b.verify_cached_uv(manifest, root)
947 if (write_metadata(archive), write_metadata(destination)) != before:
948 b.fail(
"selftest: read-only cache audit changed metadata")
950 mock.patch.object(os,
"name",
"nt"),
954 side_effect=AssertionError(
"POSIX chmod on Windows"),
960 side_effect=AssertionError(
"path-based uv probe on Windows"),
963 b.ensure_uv(manifest, root)
964 if (write_metadata(archive), write_metadata(destination)) != before:
965 b.fail(
"selftest: Windows checksum path changed POSIX metadata")
968def run_mode_selftest() -> None:
969 """Exercise authenticated POSIX modes, races, read-only audit, and Windows."""
970 if os.name ==
"posix":
971 darwin_root_alias_acceptance_selftest()
972 darwin_root_alias_rejection_selftest()
973 cache_mode_convergence_selftest()
974 cache_mode_authentication_selftest()
975 cache_open_flags_selftest()
976 cache_parent_symlink_selftest()
977 cache_parent_swap_selftest()
978 cache_atomic_parent_swap_selftest()
979 cache_nonregular_selftest()
980 cache_stable_read_selftest()
981 cache_verification_fifo_race_selftest()
982 cache_exact_fd_execution_selftest()
983 cache_same_inode_execution_selftest()
984 cache_run_exit_status_selftest()
985 cache_run_signal_status_selftest()
986 portable_readonly_fd_selftest()
987 portable_snapshot_flags_selftest()
988 for attack
in (
"replace",
"symlink",
"hardlink",
"overwrite",
"descriptor"):
989 portable_snapshot_path_attack_selftest(attack)
990 portable_snapshot_unlink_failure_selftest()
991 for target
in (
"archive",
"binary"):
992 cache_mode_path_attack_selftest(target,
"symlink", preexisting=
True)
993 for replacement
in (
"symlink",
"regular"):
994 cache_mode_path_attack_selftest(target, replacement, preexisting=
False)
995 cache_status_contract_selftest()
996 cache_mode_readonly_and_windows_selftest()