ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
bootstrap_uv_mode_selftest.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Adversarial permission and race selftests for the pinned uv bootstrap."""
4
5from __future__ import annotations
6
7import hashlib
8import importlib
9import json
10import os
11import platform
12import signal
13import socket
14import stat
15import subprocess
16import sys
17import tempfile
18from collections.abc import Callable
19from functools import partial
20from pathlib import Path
21from typing import Any, cast
22from unittest import mock
23
24b = cast(Any, globals()["bootstrap"])
25ACTUAL_UV_RUN_INVOCATION = 2
26EXPECTED_CHILD_STATUS = 37
27AUTH_ERROR_STATUS = 1
28APPLY_REQUIRED_STATUS = 2
29EXPECTED_PORTABLE_READER_OPENS = 3
30
31
32def mode_fixture(
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)
39 manifest = {
40 "schema": 1,
41 "repository": "astral-sh/uv",
42 "version": "0.0.0",
43 "assets": {key: {"name": asset_name, "sha256": hashlib.sha256(payload).hexdigest()}},
44 }
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
49 if populate:
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
56
57
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}")
64
65
66def expect_exec_failure(action: Callable[[], object], label: str) -> None:
67 """Require one execution-boundary action to fail closed."""
68 try:
69 result = action()
70 except (OSError, b.bootstrap_uv_exec.UvExecError):
71 return
72 if isinstance(result, int):
73 os.close(result)
74 b.fail(f"selftest: uv execution boundary {label} passed unexpectedly")
75
76
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."""
81
82 def wrapped_stat(
83 candidate: str | Path,
84 *,
85 dir_fd: int | None = None,
86 follow_symlinks: bool = True,
87 ) -> os.stat_result:
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:
90 fields = list(state)
91 fields[4] = owner_uid
92 return os.stat_result(fields)
93 return state
94
95 return wrapped_stat
96
97
98def open_simulated_darwin_parent(
99 root: Path,
100 alias: str,
101 *,
102 scenario: str = "valid",
103) -> int:
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
110 real_open = os.open
111 real_stat = os.stat
112 root_descriptor = real_open(root, directory_flags)
113
114 def wrapped_open(
115 candidate: str | Path,
116 flags: int,
117 mode: int = 0o777,
118 *,
119 dir_fd: int | None = None,
120 ) -> int:
121 is_alias_follow = (
122 str(candidate) == alias and dir_fd == root_descriptor and flags & os.O_NOFOLLOW == 0
123 )
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)
127
128 stat_hook = owned_stat_wrapper(
129 real_stat,
130 alias,
131 root_descriptor,
132 0 if root_owned else 1,
133 )
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()
136 with (
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),
140 ):
141 return b.bootstrap_uv_exec.open_parent_components(
142 root_descriptor,
143 (alias, "folders"),
144 directory_flags,
145 create=False,
146 platform_name=platform_name,
147 )
148
149
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:
153 root = Path(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)
159 try:
160 state = os.fstat(descriptor)
161 if (state.st_dev, state.st_ino) != (
162 physical.stat().st_dev,
163 physical.stat().st_ino,
164 ):
165 b.fail(f"selftest: Darwin /{alias} alias opened the wrong directory")
166 finally:
167 os.close(descriptor)
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(
171 root_descriptor,
172 ("private", "var", "folders"),
173 flags,
174 create=False,
175 platform_name="darwin",
176 )
177 os.close(descriptor)
178
179
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:
183 root = Path(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)
190 expect_exec_failure(
191 partial(open_simulated_darwin_parent, root, "var", scenario="owner"),
192 "owner",
193 )
194 expect_exec_failure(
195 partial(open_simulated_darwin_parent, root, "var", scenario="inode"),
196 "inode mismatch",
197 )
198 expect_exec_failure(
199 partial(open_simulated_darwin_parent, root, "var", scenario="root"),
200 "untrusted root",
201 )
202 expect_exec_failure(
203 partial(open_simulated_darwin_parent, root, "var", scenario="linux"),
204 "accepted outside Darwin",
205 )
206 with tempfile.TemporaryDirectory(prefix="ra8-uv-darwin-later-link-") as raw:
207 root = Path(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")
213
214
215def write_metadata(path: Path) -> tuple[int, int, int, int]:
216 """Return file metadata that can change only through a write-like operation."""
217 state = path.stat()
218 return stat.S_IMODE(state.st_mode), state.st_size, state.st_mtime_ns, state.st_ctime_ns
219
220
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:
226 root = Path(raw)
227 manifest, archive, destination, _, payload = mode_fixture(root, populate)
228 with (
229 mock.patch.object(subprocess, "run", return_value=probe),
230 mock.patch.object(b, "download_payload", return_value=payload),
231 ):
232 b.ensure_uv(manifest, root)
233 expect_modes(archive, destination, *b.EXPECTED_PUBLIC_MODES)
234
235
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:
240 root = Path(raw)
241 manifest, archive, destination, _, payload = mode_fixture(root)
242 if target == "archive":
243 mutated = bytearray(payload)
244 mutated[4] ^= 1
245 archive.write_bytes(mutated)
246 else:
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",
251 )
252 expect_modes(
253 archive,
254 destination,
255 b.PRIVATE_ARCHIVE_MODE,
256 b.PRIVATE_EXECUTABLE_MODE,
257 )
258
259
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")
265 real_open = os.open
266 seen: list[tuple[str, int, int | None, bool]] = []
267
268 def record_open(
269 candidate: str | Path,
270 flags: int,
271 mode: int = 0o777,
272 *,
273 dir_fd: int | None = None,
274 ) -> int:
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) == (
280 root_state.st_dev,
281 root_state.st_ino,
282 )
283 seen.append((str(candidate), flags, dir_fd, parent_is_root))
284 return real_open(candidate, flags, mode, dir_fd=dir_fd)
285
286 with mock.patch.object(os, "open", side_effect=record_open):
287 descriptor = b.open_cache_fd(path)
288 os.close(descriptor)
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")}
300 )
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",
314 )
315
316
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()
321 real = root / "real"
322 real.mkdir()
323 artifact = real / "artifact"
324 artifact.write_bytes(b"verified")
325 link = root / "link"
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",
331 )
332
333
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"
341 real_open = os.open
342 swapped = False
343
344 def swap_parent_then_open(
345 candidate: str | Path,
346 flags: int,
347 mode: int = 0o777,
348 *,
349 dir_fd: int | None = None,
350 ) -> int:
351 nonlocal swapped
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:
354 swapped = True
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)
358
359 digest = hashlib.sha256(payload).hexdigest()
360 with mock.patch.object(os, "open", side_effect=swap_parent_then_open):
361 b.expect_bootstrap_error(
362 partial(
363 b.normalize_cached_modes,
364 archive,
365 destination,
366 asset_name,
367 digest,
368 ),
369 "cache parent rename/symlink swap",
370 "moved during permission repair",
371 )
372 expect_modes(
373 displaced / archive.name,
374 displaced / destination.name,
375 b.PUBLIC_ARCHIVE_MODE,
376 b.PUBLIC_EXECUTABLE_MODE,
377 )
378
379
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"
386 external.mkdir()
387 original_parent = destination.parent
388 displaced = root / "displaced-write-parent"
389 real_replace = os.replace
390 swapped = False
391
392 def swap_parent_then_replace(
393 source: str,
394 target: str,
395 *,
396 src_dir_fd: int | None = None,
397 dst_dir_fd: int | None = None,
398 ) -> None:
399 nonlocal swapped
400 readiness = (swapped, src_dir_fd is None, dst_dir_fd is None)
401 if readiness == (False, False, False):
402 swapped = True
403 original_parent.rename(displaced)
404 original_parent.symlink_to(external, target_is_directory=True)
405 real_replace(
406 source,
407 target,
408 src_dir_fd=src_dir_fd,
409 dst_dir_fd=dst_dir_fd,
410 )
411
412 with (
413 mock.patch.object(b, "download_payload", return_value=payload),
414 mock.patch.object(os, "replace", side_effect=swap_parent_then_replace),
415 ):
416 b.expect_bootstrap_error(
417 partial(b.ensure_uv, manifest, root),
418 "atomic cache write parent swap",
419 "cannot open cached uv parent",
420 )
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")
425
426
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:
430 root = Path(raw)
431 regular = root / "regular"
432 regular.write_bytes(b"verified")
433 hardlink = root / "hardlink"
434 hardlink.hardlink_to(regular)
435 directory = root / "directory"
436 directory.mkdir()
437 fifo = root / "fifo"
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))
442
443 def timeout(_signum: int, _frame: object) -> None:
444 message = "non-regular cache open blocked"
445 raise RuntimeError(message)
446
447 previous = signal.signal(signal.SIGALRM, timeout)
448 try:
449 for path in (regular, hardlink, directory, fifo, socket_path):
450 signal.alarm(1)
451 b.expect_bootstrap_error(
452 partial(b.open_cache_fd, path),
453 f"non-regular cache node {path.name}",
454 )
455 signal.alarm(0)
456 finally:
457 signal.alarm(0)
458 signal.signal(signal.SIGALRM, previous)
459 listener.close()
460
461
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
466
467 def rewrite_then_open(duplicate: int, *args: object, **kwargs: object) -> object:
468 before = path.stat()
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)
473
474 try:
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),
478 label,
479 "changed while authenticating",
480 )
481 finally:
482 os.close(descriptor)
483
484
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)
491 try:
492 payload, _ = b.read_stable_fd(descriptor, path, 8)
493 if payload != b"12345678":
494 b.fail("selftest: maximum-sized cache read changed bytes")
495 finally:
496 os.close(descriptor)
497 path.write_bytes(b"123456789")
498 descriptor = b.open_cache_fd(path)
499 try:
500 b.expect_bootstrap_error(
501 partial(b.read_stable_fd, descriptor, path, 8),
502 "oversized cached artifact",
503 "exceeds policy",
504 )
505 finally:
506 os.close(descriptor)
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"
512 )
513
514
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:
518 root = Path(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
523 swapped = False
524
525 def swap_then_open(path: Path) -> int:
526 nonlocal swapped
527 if path == archive and not swapped:
528 swapped = True
529 archive.unlink()
530 os.mkfifo(archive, 0o600)
531 return real_open(path)
532
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",
538 )
539
540
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:
545 root = Path(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
551 calls = 0
552
553 def swap_then_run(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
554 nonlocal calls
555 calls += 1
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"
561 )
562 replacement.chmod(b.PUBLIC_EXECUTABLE_MODE)
563 destination.rename(displaced)
564 replacement.replace(destination)
565 return real_run(argv, **kwargs)
566
567 with mock.patch.object(subprocess, "run", side_effect=swap_then_run):
568 try:
569 b.run_cached_uv(manifest, root, ["work"], ensure=False)
570 except (b.CacheMetadataChangedError, b.CachePathBindingError):
571 pass
572 except b.BootstrapError:
573 b.fail("selftest: pathname replacement returned unrelated error class")
574 else:
575 b.fail("selftest: pathname replacement was not rejected")
576 if victim.exists():
577 b.fail("selftest: version probe executed unauthenticated replacement bytes")
578
579
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:
584 root = Path(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
591 calls = 0
592
593 def mutate_inode_then_run(
594 argv: list[str], **kwargs: object
595 ) -> subprocess.CompletedProcess[str]:
596 nonlocal calls
597 calls += 1
598 if calls == ACTUAL_UV_RUN_INVOCATION:
599 with destination.open("r+b") as target:
600 target.write(malicious)
601 target.truncate()
602 target.flush()
603 os.fsync(target.fileno())
604 return real_run(argv, **kwargs)
605
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",
611 )
612 if victim.exists():
613 b.fail("selftest: version probe executed post-auth cache inode bytes")
614
615
616def cache_run_exit_status_selftest() -> None:
617 """Prove the public --run mode returns the exact immutable child status."""
618 binary = (
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'
621 )
622 with tempfile.TemporaryDirectory(prefix="ra8-uv-run-status-") as raw:
623 root = Path(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"):
628 argv = [
629 sys.executable,
630 "-I",
631 "-S",
632 str(Path(b.__file__)),
633 "--manifest",
634 str(manifest),
635 "--cache-root",
636 str(root),
637 execution_mode,
638 "exit",
639 str(EXPECTED_CHILD_STATUS),
640 ]
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")
645
646
647def cache_run_signal_status_selftest() -> None:
648 """Prove public run modes reproduce an immutable child's terminating signal."""
649 binary = (
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'
652 )
653 with tempfile.TemporaryDirectory(prefix="ra8-uv-run-signal-") as raw:
654 root = Path(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"):
659 argv = [
660 sys.executable,
661 "-I",
662 "-S",
663 str(Path(b.__file__)),
664 "--manifest",
665 str(manifest),
666 "--cache-root",
667 str(root),
668 execution_mode,
669 "signal",
670 ]
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")
677
678
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")
683 executable = ""
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")
693 try:
694 os.write(descriptor, b"x")
695 except OSError:
696 pass
697 else:
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")
705
706
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):
710 pass
711
712
713def portable_snapshot_flags_selftest() -> None:
714 """Prove the named portable snapshot is reopened with every safety flag."""
715 real_open = os.open
716 reader_flags: list[int] = []
717
718 def record_open(
719 candidate: str | Path,
720 flags: int,
721 mode: int = 0o777,
722 *,
723 dir_fd: int | None = None,
724 ) -> int:
725 is_reader = str(candidate).startswith(".ra8-uv-") and flags & os.O_CREAT == 0
726 if is_reader:
727 reader_flags.append(flags)
728 return real_open(candidate, flags, mode, dir_fd=dir_fd)
729
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")
739
740
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!!"
745 real_open = os.open
746 attacked = False
747
748 def attack_open(
749 candidate: str | Path,
750 flags: int,
751 mode: int = 0o777,
752 *,
753 dir_fd: int | None = None,
754 ) -> int:
755 nonlocal attacked
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)
759 attacked = True
760 if attack in {"replace", "symlink"}:
761 os.unlink(candidate, dir_fd=dir_fd)
762 if attack in {"replace", "descriptor", "symlink"}:
763 victim = real_open(
764 ".ra8-uv-victim", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o500, dir_fd=dir_fd
765 )
766 os.write(victim, replacement)
767 os.close(victim)
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)
778 os.close(writer)
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)
783
784 with mock.patch.object(os, "open", side_effect=attack_open):
785 expect_exec_failure(
786 partial(run_portable_snapshot, binary),
787 f"portable {attack} attack",
788 )
789 if not attacked:
790 b.fail(f"selftest: portable {attack} attack did not reach the reader open")
791
792
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
796 attacked = False
797
798 def reject_unlink(candidate: str | Path, *, dir_fd: int | None = None) -> None:
799 nonlocal attacked
800 if str(candidate).startswith(".ra8-uv-") and not attacked:
801 attacked = True
802 message = "simulated unlink denial"
803 raise PermissionError(message)
804 real_unlink(candidate, dir_fd=dir_fd)
805
806 with mock.patch.object(os, "unlink", side_effect=reject_unlink):
807 expect_exec_failure(
808 partial(run_portable_snapshot, b"verified-uv"),
809 "portable unlink failure",
810 )
811 if not attacked:
812 b.fail("selftest: portable unlink attack did not reach the unlink boundary")
813
814
815def bootstrap_run_status(manifest: Path, root: Path) -> int:
816 """Return the real public --run status for one cache fixture."""
817 argv = [
818 sys.executable,
819 "-I",
820 "-S",
821 str(Path(b.__file__)),
822 "--manifest",
823 str(manifest),
824 "--cache-root",
825 str(root),
826 "--run",
827 "--version",
828 ]
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)
834
835
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:
839 root = Path(raw)
840 manifest, archive, _destination, _, _payload = mode_fixture(root)
841 try:
842 b.verify_cached_uv(manifest, root)
843 except b.CacheApplyRequiredError:
844 pass
845 else:
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")
850 try:
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")
857 else:
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")
861
862 with tempfile.TemporaryDirectory(prefix="ra8-uv-missing-mode-") as raw:
863 root = Path(raw)
864 manifest, _archive, _destination, _, _ = mode_fixture(root, populate=False)
865 try:
866 b.verify_cached_uv(manifest, root)
867 except b.CacheApplyRequiredError:
868 pass
869 else:
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")
873
874
875def cache_mode_path_attack_selftest(
876 target_name: str, replacement_kind: str, preexisting: bool
877) -> None:
878 """Prove symlink and moved-path attacks cannot redirect authenticated chmod."""
879 with tempfile.TemporaryDirectory(prefix="ra8-uv-path-mode-") as raw:
880 root = Path(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)
888
889 def replace_target() -> None:
890 """Replace the selected cache path without changing replacement bytes."""
891 target.rename(moved)
892 if replacement_kind == "symlink":
893 target.symlink_to(replacement)
894 else:
895 replacement.rename(target)
896
897 real_fchmod = os.fchmod
898 calls = 0
899
900 def swap_then_fchmod(descriptor: int, requested_mode: int) -> None:
901 nonlocal calls
902 if calls == 0 and not preexisting:
903 replace_target()
904 calls += 1
905 real_fchmod(descriptor, requested_mode)
906
907 if preexisting:
908 replace_target()
909 expected = (
910 "cannot open cached uv artifact" if preexisting else "moved during permission repair"
911 )
912 with (
913 mock.patch.object(os, "fchmod", side_effect=swap_then_fchmod),
914 mock.patch.object(os, "chmod", side_effect=AssertionError("path-based chmod")),
915 ):
916 b.expect_bootstrap_error(
917 partial(
918 b.normalize_cached_modes,
919 archive,
920 destination,
921 asset_name,
922 hashlib.sha256(payload).hexdigest(),
923 ),
924 f"{target_name} {replacement_kind} path attack",
925 expected,
926 )
927 if stat.S_IMODE(target.stat().st_mode) != mode:
928 b.fail(f"selftest: {target_name} {replacement_kind} replacement received chmod")
929
930
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:
934 root = Path(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",
944 )
945 else:
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")
949 with (
950 mock.patch.object(os, "name", "nt"),
951 mock.patch.object(
952 os,
953 "fchmod",
954 side_effect=AssertionError("POSIX chmod on Windows"),
955 create=True,
956 ),
957 mock.patch.object(
958 subprocess,
959 "run",
960 side_effect=AssertionError("path-based uv probe on Windows"),
961 ),
962 ):
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")
966
967
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()