Files
KF-OnlineJudge/backend/tests/test_admin_archive_vm.py
T

181 lines
6.5 KiB
Python

"""Finite adverse archive/private-storage acceptance: independent capped VM only."""
import hashlib
import io
import json
import os
import stat
import struct
import time
import zipfile
from pathlib import Path
from uuid import uuid4
import pytest
from test_admin_import_vm import independent_import_vm as independent_import_vm
from test_admin_import_vm import package
from kf_oj.contracts.judging_v2 import AdmissionRegistry
from kf_oj.infra.artifacts.archive_reader import ArchiveError, ArchiveReader
from kf_oj.infra.artifacts.staging import ImportStaging
from kf_oj.infra.artifacts.store import ArtifactError, ArtifactStore
ROOT = Path(__file__).resolve().parents[2]
def policy():
return AdmissionRegistry.model_validate_json(
(ROOT / "tests/fixtures/contracts-v2/judge-registry.valid.json").read_bytes()
)
def proof(name, value):
print(
"KF_FINAL_ACCEPTANCE " + json.dumps({"name": name, "result": value}, default=str),
flush=True,
)
def test_adverse_zip_and_private_storage_boundaries(independent_import_vm, tmp_path):
outside = tmp_path / "outside-sentinel"
outside.write_bytes(b"unchanged synthetic sentinel")
original = hashlib.sha256(outside.read_bytes()).hexdigest()
good, _ = package(policy())
cases = {}
for name in ("../outside-sentinel", "/outside-sentinel", "a/b", r"a\b", "a:b"):
cases["path-" + repr(name)] = package(
policy(), payload_mutation=lambda p, n=name: p.update({n: b"bad"})
)[0]
for kind in (
stat.S_IFLNK,
stat.S_IFCHR,
stat.S_IFBLK,
stat.S_IFIFO,
stat.S_IFSOCK,
stat.S_IFDIR,
):
out = io.BytesIO()
with zipfile.ZipFile(out, "w") as archive:
entry = zipfile.ZipInfo("unsafe")
entry.create_system = 3
entry.external_attr = (kind | 0o600) << 16
archive.writestr(entry, b"synthetic")
cases[f"type-{kind}"] = out.getvalue()
for duplicate in ("statement.md", "STATEMENT.MD"):
out = io.BytesIO(good)
with zipfile.ZipFile(out, "a") as archive:
archive.writestr(duplicate, b"different")
cases["duplicate-" + duplicate] = out.getvalue()
for signature in (
b"PK\x03\x04",
b"\x1f\x8b",
b"BZh",
b"\xfd7zXZ\x00",
b"Rar!\x1a\x07",
b"7z\xbc\xaf\x27\x1c",
b"X" * 257 + b"ustar",
):
cases["nested-" + signature[:6].hex()] = package(
policy(),
payload_mutation=lambda p, s=signature: p.update({"hidden.in": s + b"synthetic"}),
)[0]
central = good.index(b"PK\x01\x02")
for label, local_offset, central_offset, value, fmt in (
("encrypted", 6, 8, 1, "<H"),
("unsupported", 8, 10, 99, "<H"),
("declared-size", 22, 24, 67108865, "<I"),
("crc", 14, 16, 1, "<I"),
):
blob = bytearray(good)
struct.pack_into(fmt, blob, local_offset, value)
struct.pack_into(fmt, blob, central + central_offset, value)
cases[label] = bytes(blob)
for label, extra in (("zip64", b"\x01\x00\x00\x00"), ("unknown-extra", b"\x99\x99\x00\x00")):
out = io.BytesIO()
with zipfile.ZipFile(out, "w") as archive:
entry = zipfile.ZipInfo("data")
entry.extra = extra
archive.writestr(entry, b"synthetic")
cases[label] = out.getvalue()
out = io.BytesIO()
with zipfile.ZipFile(out, "w") as archive:
for i in range(535):
archive.writestr(f"f{i}", b"")
cases["file-count"] = out.getvalue()
# Actual bounded inflation exceeds the forged declared size. The 65 MiB stream
# is produced in chunks under the VM cap; no 65 MiB object is allocated.
out = io.BytesIO()
with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as archive:
with archive.open("inflate", "w") as member:
for _ in range(1040):
member.write(b"A" * 65536)
blob = bytearray(out.getvalue())
center = blob.index(b"PK\x01\x02")
struct.pack_into("<I", blob, 22, 67108864)
struct.pack_into("<I", blob, center + 24, 67108864)
cases["actual-inflate-over-64MiB"] = bytes(blob)
refused = []
for index, (label, blob) in enumerate(cases.items()):
file = tmp_path / f"archive-{index}"
file.write_bytes(blob)
file.chmod(0o600)
fd = os.open(file, os.O_RDONLY)
try:
with pytest.raises(ArchiveError) as error:
reader = ArchiveReader(fd, len(blob), deadline=time.monotonic() + 60)
for member in reader.members:
for _ in reader.chunks(member.name, 67108864):
pass
assert str(error.value) in ("invalid_archive", "archive_deadline")
refused.append(label)
finally:
os.close(fd)
file.unlink()
file = tmp_path / "mutable"
file.write_bytes(good)
file.chmod(0o600)
fd = os.open(file, os.O_RDONLY)
try:
reader = ArchiveReader(fd, len(good), deadline=time.monotonic() + 60)
with file.open("r+b") as writer:
writer.write(b"X")
with pytest.raises(ArchiveError, match="archive_changed"):
reader.verify_unchanged()
finally:
os.close(fd)
store = ArtifactStore(tmp_path / "objects", max_bytes=4, max_files=2)
try:
ref = store.put(b"1234")
with pytest.raises(ArtifactError, match="capacity"):
store.put(b"5")
assert store.read(ref) == b"1234"
store.remove(ref.digest)
recovered = store.put(b"5")
assert store.read(recovered) == b"5"
finally:
store.close()
link_root = tmp_path / "linked-store"
link_root.symlink_to(tmp_path / "objects", target_is_directory=True)
with pytest.raises((ArtifactError, OSError)):
ArtifactStore(link_root)
staging = ImportStaging(tmp_path / "staging")
operation_id = uuid4()
(tmp_path / "staging" / str(operation_id)).symlink_to(tmp_path, target_is_directory=True)
try:
with pytest.raises((ArtifactError, OSError)):
staging.operation(operation_id)
finally:
staging.close()
assert hashlib.sha256(outside.read_bytes()).hexdigest() == original
proof(
"archive_and_storage",
{
"refused": refused,
"outside_unchanged": True,
"content_never_executed": True,
"changed_descriptor_rejected": True,
"quota_recovered": True,
"symlink_roots_rejected": True,
},
)