Give the Git Sentry launch shape one Rust owner, update the physical corpus to supply every authority, and make the default repository gate distinguish unavailable physical prerequisites from corpus failures.
778 lines
39 KiB
Python
778 lines
39 KiB
Python
import argparse
|
|
import contextlib
|
|
import ctypes
|
|
import errno
|
|
import fcntl
|
|
import hashlib
|
|
import json
|
|
import mmap
|
|
import os
|
|
import shutil
|
|
import socket
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
FIXTURES = ROOT / "tests/fixtures/ignored-workspace-v1"
|
|
NAMESPACE_MARKER = "LAZARET_IGNORED_WORKSPACE_PRIVATE_NAMESPACE"
|
|
|
|
|
|
def command(argv, cwd=None, env=None, check=True):
|
|
return subprocess.run(
|
|
[os.fspath(value) for value in argv],
|
|
cwd=cwd,
|
|
env=env,
|
|
check=check,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
|
|
|
|
def require(condition, message):
|
|
if not condition:
|
|
raise AssertionError(message)
|
|
|
|
|
|
def expect_errno(expected, action):
|
|
try:
|
|
action()
|
|
except OSError as error:
|
|
require(error.errno == expected, f"expected errno {expected}, observed {error.errno}")
|
|
else:
|
|
raise AssertionError(f"expected errno {expected}, operation succeeded")
|
|
|
|
|
|
class MountedFixture:
|
|
def __init__(self, work_root, router, global_excludes=None):
|
|
self.root = Path(tempfile.mkdtemp(prefix="ignored-workspace-", dir=work_root))
|
|
self.host = self.root / "host"
|
|
self.isolated = self.root / "isolated"
|
|
self.mount = self.root / "mount"
|
|
for path in (self.host, self.isolated, self.mount):
|
|
path.mkdir()
|
|
command(["git", "init", "--quiet"], self.host)
|
|
command(["git", "config", "user.email", "[email protected]"], self.host)
|
|
command(["git", "config", "user.name", "Fixture"], self.host)
|
|
self.router = Path(router)
|
|
self.global_excludes = global_excludes
|
|
self.process = None
|
|
self.closed = False
|
|
|
|
def start(self):
|
|
argv = [self.router, self.host, self.isolated, self.mount]
|
|
if self.global_excludes is not None:
|
|
argv.append(self.global_excludes)
|
|
self.process = subprocess.Popen(argv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
deadline = time.monotonic() + 10
|
|
while time.monotonic() < deadline:
|
|
if self.is_mounted():
|
|
return self
|
|
if self.process.poll() is not None:
|
|
stderr = self.process.stderr.read().decode(errors="replace")
|
|
raise RuntimeError(f"router exited before mount: {stderr}")
|
|
time.sleep(0.01)
|
|
raise TimeoutError("router did not mount within ten seconds")
|
|
|
|
def stop_router(self):
|
|
if self.process is not None and self.process.poll() is None:
|
|
try:
|
|
self.process.wait(timeout=2)
|
|
except subprocess.TimeoutExpired:
|
|
self.process.terminate()
|
|
self.process.wait(timeout=10)
|
|
|
|
def is_mounted(self):
|
|
target = os.fsencode(self.mount)
|
|
for line in Path("/proc/self/mountinfo").read_bytes().splitlines():
|
|
fields = line.split(b" - ", 1)[0].split()
|
|
if len(fields) >= 5 and mountinfo_path(fields[4]) == target:
|
|
return True
|
|
return False
|
|
|
|
def force_unmount(self):
|
|
if not self.is_mounted():
|
|
return
|
|
result = command(["fusermount3", "-u", "-z", self.mount], check=False)
|
|
require(result.returncode == 0 or not self.is_mounted(), result.stderr.decode(errors="replace"))
|
|
|
|
def shutdown(self):
|
|
self.force_unmount()
|
|
self.stop_router()
|
|
require(not self.is_mounted(), "owned FUSE mount remains")
|
|
|
|
def close(self):
|
|
if self.closed:
|
|
return
|
|
try:
|
|
self.shutdown()
|
|
shutil.rmtree(self.root)
|
|
finally:
|
|
self.closed = not self.root.exists() and not self.is_mounted()
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, traceback):
|
|
self.shutdown()
|
|
|
|
|
|
def mountinfo_path(value):
|
|
return value.replace(b"\\040", b" ").replace(b"\\011", b"\t").replace(b"\\012", b"\n").replace(b"\\134", b"\\")
|
|
|
|
|
|
def write(path, data):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(data)
|
|
|
|
|
|
def commit(fixture, relative="tracked", data=b"tracked"):
|
|
write(fixture.host / relative, data)
|
|
command(["git", "add", "--", relative], fixture.host)
|
|
command(["git", "commit", "--quiet", "-m", relative], fixture.host)
|
|
|
|
|
|
def ignored_pair(fixture, relative="cache/item", host=b"host", isolated=b"isolated"):
|
|
write(fixture.host / ".gitignore", (relative.split("/", 1)[0] + "/\n").encode())
|
|
write(fixture.host / relative, host)
|
|
write(fixture.isolated / relative, isolated)
|
|
|
|
|
|
def assert_pair_unchanged(fixture, relative, host, isolated):
|
|
require((fixture.host / relative).read_bytes() == host, "host backing changed")
|
|
require((fixture.isolated / relative).read_bytes() == isolated, "isolated backing changed")
|
|
|
|
|
|
def classification_case(case_id, fixture):
|
|
if case_id == "non-utf8-ignore-match":
|
|
write(fixture.host / ".gitignore", b"*.obj\n")
|
|
host_path = os.fsencode(fixture.host) + b"/\xff\xfe.obj"
|
|
with open(host_path, "wb") as output:
|
|
output.write(b"host-sentinel")
|
|
path = os.fsencode(fixture.mount) + b"/\xff\xfe.obj"
|
|
with fixture.start():
|
|
require(not os.path.exists(path), "non-UTF8 host sentinel was exposed")
|
|
descriptor = os.open(path, os.O_CREAT | os.O_WRONLY, 0o600)
|
|
os.write(descriptor, b"isolated")
|
|
os.close(descriptor)
|
|
require(os.path.exists(os.fsencode(fixture.isolated) + b"/\xff\xfe.obj"), "non-UTF8 path was not isolated")
|
|
require(Path(os.fsdecode(host_path)).read_bytes() == b"host-sentinel", "non-UTF8 host sentinel changed")
|
|
return
|
|
if case_id == "root-negation-precedence":
|
|
write(fixture.host / ".gitignore", b"*.log\n!important.log\n")
|
|
with fixture.start():
|
|
write(fixture.mount / "ordinary.log", b"isolated")
|
|
write(fixture.mount / "important.log", b"host")
|
|
require((fixture.isolated / "ordinary.log").read_bytes() == b"isolated", "root ignore did not route")
|
|
require((fixture.host / "important.log").read_bytes() == b"host", "negation did not restore host route")
|
|
return
|
|
if case_id == "nested-ignore-precedence":
|
|
write(fixture.host / ".gitignore", b"*.tmp\n!nested/drop.tmp\n")
|
|
write(fixture.host / "nested/.gitignore", b"drop.tmp\n!keep.tmp\n")
|
|
with fixture.start():
|
|
write(fixture.mount / "nested/drop.tmp", b"isolated")
|
|
write(fixture.mount / "nested/keep.tmp", b"host")
|
|
require((fixture.isolated / "nested/drop.tmp").is_file(), "nested ignored path not isolated")
|
|
require((fixture.host / "nested/keep.tmp").is_file(), "nested negation not host-backed")
|
|
return
|
|
if case_id == "info-exclude":
|
|
write(fixture.host / ".git/info/exclude", b"local.secret\n")
|
|
write(fixture.host / "local.secret", b"host-sentinel")
|
|
with fixture.start():
|
|
require(not (fixture.mount / "local.secret").exists(), "info/exclude host sentinel was exposed")
|
|
return
|
|
if case_id == "global-excludes":
|
|
global_file = fixture.root / "global-ignore"
|
|
write(global_file, b"*.swap\n")
|
|
fixture.global_excludes = global_file
|
|
write(fixture.host / "editor.swap", b"host-sentinel")
|
|
with fixture.start():
|
|
require(not (fixture.mount / "editor.swap").exists(), "global exclude host sentinel was exposed")
|
|
return
|
|
if case_id == "ignore-case-folding":
|
|
command(["git", "config", "core.ignoreCase", "true"], fixture.host)
|
|
write(fixture.host / ".gitignore", b"build.log\n")
|
|
write(fixture.host / "BUILD.LOG", b"host-sentinel")
|
|
with fixture.start():
|
|
require(not (fixture.mount / "BUILD.LOG").exists(), "case-folded host sentinel was exposed")
|
|
return
|
|
if case_id == "nested-repository-boundary":
|
|
write(fixture.host / ".gitignore", b"*.cache\n")
|
|
nested = fixture.host / "vendor/nested"
|
|
nested.mkdir(parents=True)
|
|
command(["git", "init", "--quiet"], nested)
|
|
write(nested / ".gitignore", b"target/\n")
|
|
write(nested / "target/sentinel", b"host-sentinel")
|
|
with fixture.start():
|
|
require(not (fixture.mount / "vendor/nested/target/sentinel").exists(), "nested host sentinel was exposed")
|
|
write(fixture.mount / "vendor/nested/target/item", b"isolated")
|
|
write(fixture.mount / "vendor/nested/ordinary.cache", b"host")
|
|
require((fixture.isolated / "vendor/nested/target/item").is_file(), "nested policy not selected")
|
|
require((fixture.host / "vendor/nested/ordinary.cache").is_file(), "parent policy crossed repository boundary")
|
|
return
|
|
if case_id == "live-ignore-publication":
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
|
write(fixture.host / "cache/item", b"host")
|
|
with fixture.start():
|
|
require((fixture.mount / "cache/item").read_bytes() == b"host", "initial host route missing")
|
|
write(fixture.host / ".gitignore", b"cache/\n")
|
|
require(not (fixture.mount / "cache/item").exists(), "published ignore did not hide host object")
|
|
require((fixture.host / "cache/item").read_bytes() == b"host", "publication copied or removed host object")
|
|
return
|
|
raise AssertionError(f"classification case is not implemented: {case_id}")
|
|
|
|
|
|
def atomic_case(case_id, fixture):
|
|
if case_id == "cross-route-directory-exchange":
|
|
write(fixture.host / ".gitignore", b"ignored/\n")
|
|
(fixture.host / "source").mkdir()
|
|
write(fixture.host / "source/marker", b"host")
|
|
(fixture.isolated / "ignored/destination").mkdir(parents=True)
|
|
write(fixture.isolated / "ignored/destination/marker", b"isolated")
|
|
else:
|
|
write(fixture.host / ".gitignore", b"ignored/\n")
|
|
write(fixture.host / "source", b"source")
|
|
if case_id in {"cross-route-file-rename", "cross-route-hard-link"}:
|
|
(fixture.isolated / "ignored").mkdir()
|
|
with fixture.start():
|
|
if case_id == "cross-route-hard-link":
|
|
expect_errno(errno.EXDEV, lambda: os.link(fixture.mount / "source", fixture.mount / "ignored/destination"))
|
|
elif case_id == "cross-route-directory-exchange":
|
|
rename_exchange(fixture.mount / "source", fixture.mount / "ignored/destination")
|
|
else:
|
|
expect_errno(errno.EXDEV, lambda: os.rename(fixture.mount / "source", fixture.mount / "ignored/destination"))
|
|
if case_id == "cross-route-directory-exchange":
|
|
require((fixture.host / "source/marker").read_bytes() == b"host", "source directory mutated after EXDEV")
|
|
require((fixture.isolated / "ignored/destination/marker").read_bytes() == b"isolated", "destination directory mutated after EXDEV")
|
|
else:
|
|
require((fixture.host / "source").read_bytes() == b"source", "source mutated after EXDEV")
|
|
require(not (fixture.host / "ignored/destination").exists() and not (fixture.isolated / "ignored/destination").exists(), "destination appeared after EXDEV")
|
|
|
|
|
|
def rename_exchange(source, destination):
|
|
observed = exchange_paths(source, destination)
|
|
if observed == 0:
|
|
raise AssertionError("cross-route RENAME_EXCHANGE succeeded")
|
|
require(observed == errno.EXDEV, f"expected EXDEV from RENAME_EXCHANGE, observed {observed}")
|
|
|
|
|
|
def exchange_paths(source, destination):
|
|
libc = ctypes.CDLL(None, use_errno=True)
|
|
result = libc.renameat2(-100, os.fsencode(source), -100, os.fsencode(destination), 2)
|
|
if result == 0:
|
|
return 0
|
|
return ctypes.get_errno()
|
|
|
|
|
|
def copy_file_range(source, destination, length):
|
|
libc = ctypes.CDLL(None, use_errno=True)
|
|
syscall = libc.copy_file_range
|
|
syscall.argtypes = [
|
|
ctypes.c_int,
|
|
ctypes.POINTER(ctypes.c_longlong),
|
|
ctypes.c_int,
|
|
ctypes.POINTER(ctypes.c_longlong),
|
|
ctypes.c_size_t,
|
|
ctypes.c_uint,
|
|
]
|
|
syscall.restype = ctypes.c_ssize_t
|
|
copied = 0
|
|
while copied < length:
|
|
result = syscall(source, None, destination, None, length - copied, 0)
|
|
if result < 0:
|
|
observed = ctypes.get_errno()
|
|
raise OSError(observed, os.strerror(observed))
|
|
if result == 0:
|
|
break
|
|
copied += result
|
|
return copied
|
|
|
|
|
|
def policy_transition_case(case_id, fixture):
|
|
ignored_pair(fixture)
|
|
if case_id == "non-ignored-becomes-ignored":
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
|
with fixture.start():
|
|
if case_id == "ignored-becomes-non-ignored":
|
|
require((fixture.mount / "cache/item").read_bytes() == b"isolated", "initial isolated route missing")
|
|
old_descriptor = os.open(fixture.mount / "cache/item", os.O_RDWR)
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
|
require((fixture.mount / "cache/item").read_bytes() == b"host", "host route not selected")
|
|
os.pwrite(old_descriptor, b"old-isolated", 0)
|
|
os.close(old_descriptor)
|
|
require((fixture.isolated / "cache/item").read_bytes().startswith(b"old-isolated"), "old handle rebound from isolated backing")
|
|
else:
|
|
require((fixture.mount / "cache/item").read_bytes() == b"host", "initial host route missing")
|
|
old_descriptor = os.open(fixture.mount / "cache/item", os.O_RDWR)
|
|
write(fixture.host / ".gitignore", b"cache/\n")
|
|
require((fixture.mount / "cache/item").read_bytes() == b"isolated", "isolated route not selected")
|
|
os.pwrite(old_descriptor, b"old-host", 0)
|
|
os.close(old_descriptor)
|
|
require((fixture.host / "cache/item").read_bytes().startswith(b"old-host"), "old handle rebound from host backing")
|
|
if case_id == "ignored-becomes-non-ignored":
|
|
require((fixture.host / "cache/item").read_bytes() == b"host", "transition copied to host")
|
|
else:
|
|
require((fixture.isolated / "cache/item").read_bytes() == b"isolated", "transition copied to isolated backing")
|
|
|
|
|
|
def index_case(case_id, fixture):
|
|
ignored_pair(fixture, "deps/lib")
|
|
with fixture.start():
|
|
if case_id == "guest-force-add-isolated-only":
|
|
(fixture.host / "deps/lib").unlink()
|
|
initial_index = (fixture.host / ".git/index").read_bytes() if (fixture.host / ".git/index").exists() else None
|
|
result = command(["git", "add", "-f", "--", "deps/lib"], fixture.mount, check=False)
|
|
require(result.returncode != 0, "isolated-only force-add succeeded")
|
|
final_index = (fixture.host / ".git/index").read_bytes() if (fixture.host / ".git/index").exists() else None
|
|
require(final_index == initial_index, "refused force-add published an index")
|
|
require((fixture.isolated / "deps/lib").read_bytes() == b"isolated", "isolated object was lost")
|
|
require(not (fixture.host / "deps/lib").exists(), "refused force-add created a host object")
|
|
elif case_id == "host-established-tracked-cutover":
|
|
(fixture.host / "deps/lib").unlink()
|
|
old_descriptor = os.open(fixture.mount / "deps/lib", os.O_RDWR)
|
|
write(fixture.host / "deps/lib", b"independent-host")
|
|
command(["git", "add", "-f", "--", "deps/lib"], fixture.host)
|
|
new_descriptor = os.open(fixture.mount / "deps/lib", os.O_RDONLY)
|
|
require(os.read(new_descriptor, 64) == b"independent-host", "new handle did not select host object")
|
|
os.pwrite(old_descriptor, b"old-handle", 0)
|
|
os.close(old_descriptor)
|
|
os.close(new_descriptor)
|
|
require((fixture.host / "deps/lib").read_bytes() == b"independent-host", "old handle rebound to host")
|
|
require((fixture.isolated / "deps/lib").read_bytes().startswith(b"old-handle"), "old handle did not remain isolated")
|
|
else:
|
|
(fixture.host / "deps/lib").unlink()
|
|
command(["git", "update-index", "--add", "--cacheinfo", "100644", "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", "deps/lib"], fixture.host)
|
|
expect_errno(errno.EIO, lambda: (fixture.mount / "deps/lib").read_bytes())
|
|
require((fixture.isolated / "deps/lib").read_bytes() == b"isolated", "inconsistent isolated object was lost")
|
|
|
|
|
|
def open_handle_case(case_id, fixture):
|
|
ignored_pair(fixture, "cache/pinned", host=b"host-data", isolated=b"isolated-data")
|
|
with fixture.start():
|
|
if case_id == "mmap-policy-transition":
|
|
descriptor = os.open(fixture.mount / "cache/pinned", os.O_RDWR)
|
|
mapping = mmap.mmap(descriptor, 0, access=mmap.ACCESS_WRITE)
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
|
mapping[0:8] = b"mapped!!"
|
|
mapping.flush()
|
|
mapping.close()
|
|
os.close(descriptor)
|
|
require((fixture.isolated / "cache/pinned").read_bytes().startswith(b"mapped!!"), "mapping rebound")
|
|
require((fixture.host / "cache/pinned").read_bytes() == b"host-data", "mapping copied back")
|
|
elif case_id == "directory-stream-policy-snapshot":
|
|
write(fixture.isolated / "cache/isolated-child", b"isolated")
|
|
write(fixture.host / "cache/host-child", b"host")
|
|
iterator = os.scandir(fixture.mount / "cache")
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
|
names = {entry.name for entry in iterator}
|
|
iterator.close()
|
|
require("isolated-child" in names and "host-child" not in names, "old stream mixed snapshots")
|
|
new_names = {entry.name for entry in os.scandir(fixture.mount / "cache")}
|
|
require("host-child" in new_names and "isolated-child" not in new_names, "new stream did not use new snapshot")
|
|
else:
|
|
descriptor = os.open(fixture.mount / "cache/pinned", os.O_RDWR)
|
|
fcntl.lockf(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB, 1)
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
|
new_descriptor = os.open(fixture.mount / "cache/pinned", os.O_RDWR)
|
|
fcntl.lockf(new_descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB, 1)
|
|
require(lock_attempt(fixture.isolated / "cache/pinned") == errno.EAGAIN, "old isolated lock was transferred or lost")
|
|
require(lock_attempt(fixture.host / "cache/pinned") == errno.EAGAIN, "new host lock was not established")
|
|
os.close(descriptor)
|
|
require(lock_attempt(fixture.isolated / "cache/pinned") == 0, "isolated lock remained after old handle closed")
|
|
require(lock_attempt(fixture.host / "cache/pinned") == errno.EAGAIN, "isolated unlock affected host lock")
|
|
os.close(new_descriptor)
|
|
require(lock_attempt(fixture.host / "cache/pinned") == 0, "host lock remained after new handle closed")
|
|
|
|
|
|
def lock_attempt(path):
|
|
source = """
|
|
import fcntl, os, sys
|
|
descriptor = os.open(sys.argv[1], os.O_RDWR)
|
|
try:
|
|
fcntl.lockf(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB, 1)
|
|
except OSError as error:
|
|
sys.exit(error.errno)
|
|
fcntl.lockf(descriptor, fcntl.LOCK_UN, 1)
|
|
"""
|
|
return subprocess.run([sys.executable, "-c", source, path], check=False).returncode
|
|
|
|
|
|
def operation_case(case_id, fixture):
|
|
ignored_pair(fixture, "cache/base")
|
|
if case_id == "timestamps":
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
|
with fixture.start():
|
|
if case_id == "extended-attributes-and-acls":
|
|
path = fixture.mount / "cache/base"
|
|
os.setxattr(path, b"user.lazaret", b"value")
|
|
require(os.getxattr(path, b"user.lazaret") == b"value", "xattr round trip failed")
|
|
require(os.getxattr(fixture.isolated / "cache/base", b"user.lazaret") == b"value", "xattr reached wrong backing")
|
|
os.removexattr(path, b"user.lazaret")
|
|
expect_errno(errno.ENODATA, lambda: os.getxattr(fixture.isolated / "cache/base", b"user.lazaret"))
|
|
if shutil.which("setfacl"):
|
|
command(["setfacl", "-m", f"u:{os.getuid()}:rw", path])
|
|
mounted_acl = command(["getfacl", "--absolute-names", path]).stdout
|
|
backing_acl = command(["getfacl", "--absolute-names", fixture.isolated / "cache/base"]).stdout
|
|
require(b"user:" in mounted_acl and b"user:" in backing_acl, "POSIX ACL was not retained on selected backing")
|
|
elif case_id == "sparse-copy-range":
|
|
source = fixture.mount / "cache/source"
|
|
destination = fixture.mount / "cache/destination"
|
|
with source.open("wb") as output:
|
|
output.write(b"x" * 8192)
|
|
output.seek(1024 * 1024)
|
|
output.write(b"tail")
|
|
descriptor = os.open(source, os.O_RDONLY)
|
|
require(os.lseek(descriptor, 0, os.SEEK_DATA) == 0, "SEEK_DATA did not find first extent")
|
|
require(os.lseek(descriptor, 0, os.SEEK_HOLE) >= 8192, "SEEK_HOLE did not find sparse gap")
|
|
os.close(descriptor)
|
|
command(["fallocate", "--punch-hole", "--keep-size", "--offset", "0", "--length", "4096", source])
|
|
require(source.read_bytes()[:4096] == bytes(4096), "hole punch retained data")
|
|
with source.open("rb") as input_file, destination.open("wb") as output:
|
|
require(
|
|
copy_file_range(input_file.fileno(), output.fileno(), 1024 * 1024 + 4)
|
|
== 1024 * 1024 + 4,
|
|
"same-route copy range ended early",
|
|
)
|
|
require(input_file.tell() == 1024 * 1024 + 4, "same-route source offset differs")
|
|
require(output.tell() == 1024 * 1024 + 4, "same-route destination offset differs")
|
|
require(destination.read_bytes() == source.read_bytes(), "same-route copy differs")
|
|
require(destination.stat().st_size == source.stat().st_size, "same-route copy size differs")
|
|
write(fixture.host / "host-copy", b"unchanged")
|
|
write(fixture.host / "unrelated-host", b"host-sentinel")
|
|
write(fixture.isolated / "cache/unrelated-isolated", b"isolated-sentinel")
|
|
source_before = (fixture.isolated / "cache/source").read_bytes()
|
|
same_route_destination_before = (fixture.isolated / "cache/destination").read_bytes()
|
|
with (fixture.mount / "host-copy").open("r+b") as output, source.open("rb") as input_file:
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
|
os.lseek(input_file.fileno(), 4096, os.SEEK_SET)
|
|
require(copy_file_range(input_file.fileno(), output.fileno(), 1) == 1, "cross-route copy count differs")
|
|
require((fixture.host / "host-copy").read_bytes() == b"xnchanged", "cross-route destination range differs")
|
|
require((fixture.isolated / "cache/source").read_bytes() == source_before, "cross-route copy mutated source")
|
|
require(
|
|
(fixture.isolated / "cache/destination").read_bytes() == same_route_destination_before,
|
|
"cross-route copy mutated same-route destination",
|
|
)
|
|
require((fixture.host / "cache/source").exists() is False, "cross-route source handle was reclassified")
|
|
require((fixture.host / "unrelated-host").read_bytes() == b"host-sentinel", "cross-route copy mutated unrelated host object")
|
|
require(
|
|
(fixture.isolated / "cache/unrelated-isolated").read_bytes() == b"isolated-sentinel",
|
|
"cross-route copy mutated unrelated isolated object",
|
|
)
|
|
elif case_id == "symlink-confinement":
|
|
os.symlink("../../outside", fixture.mount / "cache/link")
|
|
require(os.readlink(fixture.mount / "cache/link") == "../../outside", "symlink target changed")
|
|
write(fixture.host / "virtual-target", b"virtual-resolution")
|
|
os.symlink("../virtual-target", fixture.mount / "cache/virtual-link")
|
|
require(
|
|
(fixture.mount / "cache/virtual-link").read_bytes() == b"virtual-resolution",
|
|
"virtual symlink resolution changed",
|
|
)
|
|
command(
|
|
[
|
|
"cargo",
|
|
"nextest",
|
|
"run",
|
|
"--offline",
|
|
"--locked",
|
|
"-p",
|
|
"lazaret-ignored-workspace",
|
|
"--test-threads",
|
|
"2",
|
|
"-E",
|
|
"test(descriptor_traversal_race_never_follows_backing_symlinks)",
|
|
],
|
|
ROOT,
|
|
)
|
|
elif case_id == "timestamps":
|
|
path = fixture.mount / "cache/base"
|
|
stamp = 1_700_000_000_123_456_789
|
|
os.utime(path, ns=(stamp, stamp), follow_symlinks=False)
|
|
metadata = os.stat(path, follow_symlinks=False)
|
|
require(metadata.st_mtime_ns == stamp, "nanosecond timestamp changed")
|
|
require(os.stat(fixture.host / "cache/base", follow_symlinks=False).st_mtime_ns == stamp, "timestamp was not bidirectional")
|
|
else:
|
|
fifo = fixture.mount / "cache/fifo"
|
|
os.mkfifo(fifo)
|
|
with socket.socket(socket.AF_UNIX) as endpoint:
|
|
original_directory = os.open(".", os.O_RDONLY | os.O_DIRECTORY)
|
|
try:
|
|
os.chdir(fixture.mount)
|
|
endpoint.bind("cache/socket")
|
|
finally:
|
|
os.fchdir(original_directory)
|
|
os.close(original_directory)
|
|
require(stat.S_ISFIFO(os.lstat(fixture.isolated / "cache/fifo").st_mode), "FIFO did not reach isolated backing")
|
|
require(stat.S_ISSOCK(os.lstat(fixture.isolated / "cache/socket").st_mode), "socket did not reach isolated backing")
|
|
expect_errno(errno.EPERM, lambda: os.mknod(fixture.mount / "cache/device", stat.S_IFCHR | 0o600, os.makedev(1, 3)))
|
|
|
|
|
|
def git_case(case_id, fixture):
|
|
commit(fixture)
|
|
initial_branch = command(["git", "symbolic-ref", "--short", "HEAD"], fixture.host).stdout.strip()
|
|
ignored_pair(fixture, "cache/sentinel")
|
|
with fixture.start():
|
|
if case_id == "git-ordinary-workflow":
|
|
write(fixture.mount / "ordinary", b"ordinary")
|
|
command(["git", "status", "--porcelain=v2"], fixture.mount)
|
|
command(["git", "add", "ordinary"], fixture.mount)
|
|
command(["git", "commit", "--quiet", "-m", "ordinary"], fixture.mount)
|
|
write(fixture.mount / "tracked", b"changed")
|
|
command(["git", "restore", "tracked"], fixture.mount)
|
|
require((fixture.host / "ordinary").read_bytes() == b"ordinary", "ordinary commit was not bidirectional")
|
|
require((fixture.host / "tracked").read_bytes() == b"tracked", "restore did not reach host worktree")
|
|
elif case_id == "git-history-workflow":
|
|
command(["git", "checkout", "-b", "topic"], fixture.mount)
|
|
write(fixture.mount / "topic-file", b"topic")
|
|
command(["git", "add", "topic-file"], fixture.mount)
|
|
command(["git", "commit", "--quiet", "-m", "topic"], fixture.mount)
|
|
command(["git", "checkout", initial_branch], fixture.mount)
|
|
write(fixture.mount / "base-file", b"base")
|
|
command(["git", "add", "base-file"], fixture.mount)
|
|
command(["git", "commit", "--quiet", "-m", "base"], fixture.mount)
|
|
command(["git", "checkout", "topic"], fixture.mount)
|
|
command(["git", "rebase", initial_branch], fixture.mount)
|
|
command(["git", "checkout", initial_branch], fixture.mount)
|
|
command(["git", "merge", "topic"], fixture.mount)
|
|
write(fixture.mount / "tracked", b"stash")
|
|
command(["git", "stash"], fixture.mount)
|
|
command(["git", "stash", "pop"], fixture.mount)
|
|
command(["git", "restore", "tracked"], fixture.mount)
|
|
require(command(["git", "rev-parse", "topic"], fixture.mount).stdout == command(["git", "rev-parse", "topic"], fixture.host).stdout, "reference update was not bidirectional")
|
|
else:
|
|
raise AssertionError("linked-worktree and Sentry cases use dedicated harnesses")
|
|
assert_pair_unchanged(fixture, "cache/sentinel", b"host", b"isolated")
|
|
|
|
|
|
def linked_worktree_case(case_id, fixture):
|
|
del case_id
|
|
shutil.rmtree(fixture.host)
|
|
base = fixture.root / "base"
|
|
base.mkdir()
|
|
command(["git", "init", "--quiet"], base)
|
|
command(["git", "config", "user.email", "[email protected]"], base)
|
|
command(["git", "config", "user.name", "Fixture"], base)
|
|
write(base / "tracked", b"tracked")
|
|
command(["git", "add", "tracked"], base)
|
|
command(["git", "commit", "--quiet", "-m", "base"], base)
|
|
command(["git", "worktree", "add", "--quiet", fixture.host], base)
|
|
write(fixture.host / ".gitignore", b"target/\n")
|
|
write(fixture.host / "target/item", b"host-sentinel")
|
|
with fixture.start():
|
|
require(not (fixture.mount / "target/item").exists(), "linked-worktree host sentinel was exposed")
|
|
write(fixture.mount / "target/item", b"isolated")
|
|
result = command(["git", "status", "--porcelain=v2", "--untracked-files=all"], fixture.mount)
|
|
require(b"target/item" not in result.stdout, "linked-worktree ignore policy was not applied")
|
|
require((fixture.isolated / "target/item").read_bytes() == b"isolated", "linked-worktree isolated state missing")
|
|
require((fixture.host / "target/item").read_bytes() == b"host-sentinel", "linked-worktree host sentinel changed")
|
|
|
|
|
|
def sentry_case(case_id, fixture, sentry_binary):
|
|
del case_id
|
|
commit(fixture)
|
|
transient = fixture.root / "transient-hooks"
|
|
transient.mkdir()
|
|
transient_config = fixture.root / "transient-config"
|
|
transient_config.touch()
|
|
write(fixture.host / ".git/hooks/host-sentinel", b"host")
|
|
host_config = (fixture.host / ".git/config").read_bytes()
|
|
with fixture.start():
|
|
process = subprocess.Popen(
|
|
git_sentry_command(
|
|
sentry_binary,
|
|
fixture.mount,
|
|
fixture.host / ".git",
|
|
transient,
|
|
transient_config,
|
|
),
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
try:
|
|
require(process.stdout.readline() == b"ready\n", "Git Sentry did not become ready")
|
|
expect_errno(errno.EROFS, lambda: write(fixture.mount / ".git/config", b"changed"))
|
|
require(not (fixture.mount / ".git/hooks/host-sentinel").exists(), "host hook remained visible")
|
|
write(fixture.mount / ".git/hooks/sandbox-hook", b"transient")
|
|
command(["git", "status", "--porcelain=v2"], fixture.mount)
|
|
finally:
|
|
process.stdin.close()
|
|
return_code = process.wait(timeout=10)
|
|
require(return_code == 0, process.stderr.read().decode(errors="replace"))
|
|
require((fixture.mount / ".git/config").read_bytes() == host_config, "host config was not restored")
|
|
require((fixture.mount / ".git/hooks/host-sentinel").read_bytes() == b"host", "host hooks were not restored")
|
|
require((fixture.host / ".git/hooks/host-sentinel").read_bytes() == b"host", "host hook changed")
|
|
require((transient / "sandbox-hook").read_bytes() == b"transient", "transient hook missing")
|
|
|
|
|
|
def git_sentry_command(binary, workspace, common, hooks, config):
|
|
return [binary, workspace, common, hooks, config]
|
|
|
|
|
|
def invalidation_or_race_case(case_id, fixture):
|
|
ignored_pair(fixture)
|
|
with fixture.start():
|
|
if case_id == "concurrent-host-sandbox-mutation":
|
|
barrier = threading.Barrier(2)
|
|
errors = []
|
|
def host_replace():
|
|
try:
|
|
barrier.wait()
|
|
write(fixture.host / "ordinary.tmp", b"replacement")
|
|
os.replace(fixture.host / "ordinary.tmp", fixture.host / "ordinary")
|
|
except BaseException as error:
|
|
errors.append(error)
|
|
write(fixture.host / "ordinary", b"initial")
|
|
require((fixture.mount / "ordinary").read_bytes() == b"initial", "initial host bytes differ")
|
|
write(fixture.host / "ordinary", b"changed")
|
|
require((fixture.mount / "ordinary").read_bytes() == b"changed", "same-size host overwrite was stale")
|
|
thread = threading.Thread(target=host_replace)
|
|
thread.start()
|
|
barrier.wait()
|
|
with contextlib.suppress(FileNotFoundError):
|
|
os.replace(fixture.mount / "ordinary", fixture.mount / "ordinary-renamed")
|
|
thread.join()
|
|
require(not errors, f"host racer failed: {errors}")
|
|
names = [entry.name for entry in os.scandir(fixture.mount)]
|
|
require(len(names) == len(set(names)), "concurrent enumeration duplicated a name")
|
|
candidates = [name for name in ("ordinary", "ordinary-renamed") if (fixture.mount / name).exists()]
|
|
require(candidates, "native rename race lost every host-backed outcome")
|
|
write(fixture.mount / candidates[0], b"sandbox-write")
|
|
(fixture.mount / candidates[0]).unlink()
|
|
require((fixture.isolated / "cache/item").read_bytes() == b"isolated", "race crossed backing")
|
|
else:
|
|
fixture.process.kill()
|
|
fixture.process.wait(timeout=10)
|
|
try:
|
|
(fixture.mount / "cache/item").read_bytes()
|
|
except OSError as error:
|
|
require(error.errno in {errno.ENOTCONN, errno.EIO}, f"unexpected dead-router errno: {error.errno}")
|
|
else:
|
|
raise AssertionError("dead router fell through to host backing")
|
|
fixture.force_unmount()
|
|
assert_pair_unchanged(fixture, "cache/item", b"host", b"isolated")
|
|
fixture.start()
|
|
require((fixture.mount / "cache/item").read_bytes() == b"isolated", "recovered router selected wrong backing")
|
|
|
|
|
|
CASE_GROUPS = {
|
|
**{name: classification_case for name in [
|
|
"non-utf8-ignore-match", "root-negation-precedence", "nested-ignore-precedence",
|
|
"info-exclude", "global-excludes", "ignore-case-folding", "nested-repository-boundary",
|
|
"live-ignore-publication",
|
|
]},
|
|
**{name: atomic_case for name in ["cross-route-file-rename", "cross-route-directory-exchange", "cross-route-hard-link"]},
|
|
**{name: policy_transition_case for name in ["ignored-becomes-non-ignored", "non-ignored-becomes-ignored"]},
|
|
**{name: index_case for name in ["guest-force-add-isolated-only", "host-established-tracked-cutover", "unmediated-inconsistent-index"]},
|
|
**{name: open_handle_case for name in ["mmap-policy-transition", "locks-policy-transition", "directory-stream-policy-snapshot"]},
|
|
**{name: operation_case for name in ["extended-attributes-and-acls", "sparse-copy-range", "symlink-confinement", "timestamps", "special-files"]},
|
|
**{name: git_case for name in ["git-ordinary-workflow", "git-history-workflow"]},
|
|
**{name: invalidation_or_race_case for name in ["concurrent-host-sandbox-mutation", "router-loss-fails-closed"]},
|
|
}
|
|
|
|
|
|
def run_corpus(arguments):
|
|
inventory = json.loads((FIXTURES / "corpus.json").read_text())["cases"]
|
|
selected = [case for case in inventory if arguments.case in (None, case["id"])]
|
|
require(selected, "requested corpus case does not exist")
|
|
missing = [case["id"] for case in inventory if case["id"] not in CASE_GROUPS and case["id"] not in {"linked-worktree-index", "git-sentry"}]
|
|
require(not missing, f"corpus adapter is incomplete: {missing}")
|
|
results = []
|
|
Path(arguments.work_root).mkdir(parents=True, exist_ok=True)
|
|
for case in selected:
|
|
identifier = case["id"]
|
|
started = time.monotonic_ns()
|
|
fixture = MountedFixture(arguments.work_root, arguments.router)
|
|
try:
|
|
if identifier == "linked-worktree-index":
|
|
linked_worktree_case(identifier, fixture)
|
|
elif identifier == "git-sentry":
|
|
sentry_case(identifier, fixture, arguments.git_sentry)
|
|
else:
|
|
CASE_GROUPS[identifier](identifier, fixture)
|
|
disposition = "PASS"
|
|
detail = None
|
|
except BaseException as error:
|
|
disposition = "FAIL"
|
|
detail = repr(error)
|
|
finally:
|
|
try:
|
|
fixture.close()
|
|
except BaseException as cleanup_error:
|
|
if disposition == "PASS":
|
|
disposition = "FAIL"
|
|
detail = f"cleanup failed: {cleanup_error!r}"
|
|
else:
|
|
detail = f"{detail}; cleanup failed: {cleanup_error!r}"
|
|
results.append({"id": identifier, "disposition": disposition, "elapsed_ns": time.monotonic_ns() - started, "detail": detail})
|
|
if disposition != "PASS" and arguments.fail_fast:
|
|
break
|
|
output = {"schema": "lazaret-ignored-workspace-corpus-result-v1", "results": results}
|
|
Path(arguments.output).parent.mkdir(parents=True, exist_ok=True)
|
|
Path(arguments.output).write_text(json.dumps(output, indent=2, sort_keys=True) + "\n")
|
|
require(all(result["disposition"] == "PASS" for result in results), "one or more corpus cases failed")
|
|
|
|
|
|
def unavailable_physical_prerequisite():
|
|
if sys.platform != "linux":
|
|
return "Linux is required"
|
|
for program in ("git", "unshare", "fusermount3"):
|
|
if shutil.which(program) is None:
|
|
return f"{program} is unavailable"
|
|
fuse = Path("/dev/fuse")
|
|
if not fuse.exists() or not os.access(fuse, os.R_OK | os.W_OK):
|
|
return "/dev/fuse is unavailable"
|
|
probe = command(
|
|
["unshare", "--user", "--map-root-user", "--mount", "--propagation", "private", "--", "true"],
|
|
check=False,
|
|
)
|
|
if probe.returncode != 0:
|
|
return "private user and mount namespaces are unavailable"
|
|
return None
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
corpus = subparsers.add_parser("corpus")
|
|
corpus.add_argument("--router", type=Path, required=True)
|
|
corpus.add_argument("--git-sentry", type=Path, required=True)
|
|
corpus.add_argument("--work-root", type=Path, required=True)
|
|
corpus.add_argument("--output", type=Path, required=True)
|
|
corpus.add_argument("--case")
|
|
corpus.add_argument("--fail-fast", action="store_true")
|
|
arguments = parser.parse_args()
|
|
if arguments.command == "corpus":
|
|
unavailable = unavailable_physical_prerequisite()
|
|
if unavailable is not None:
|
|
print(f"SKIP ignored-workspace corpus: {unavailable}", file=sys.stderr)
|
|
return 77
|
|
if arguments.case in (None, "git-sentry") and os.environ.get(NAMESPACE_MARKER) != "1":
|
|
environment = os.environ.copy()
|
|
environment[NAMESPACE_MARKER] = "1"
|
|
os.execvpe(
|
|
"unshare",
|
|
[
|
|
"unshare",
|
|
"--user",
|
|
"--map-root-user",
|
|
"--mount",
|
|
"--propagation",
|
|
"private",
|
|
"--",
|
|
sys.executable,
|
|
os.fspath(Path(__file__).resolve()),
|
|
*sys.argv[1:],
|
|
],
|
|
environment,
|
|
)
|
|
run_corpus(arguments)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|