This commit is contained in:
2026-08-18 09:20:44 +02:00
commit b826cd721a
522 changed files with 93730 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
umask 022
spec=$PRE15_DUT/tests/pre15/fixtures/B01-g3-archives.json
helper=$PRE15_DUT/tests/pre15/fixtures/g3.py
metadata_archive=$PRE15_DUT/tests/results/manual/2026-08-08T2337Z-metadata-vfs/prepare-fixtures.sh
final_archive=$PRE15_DUT/tests/results/manual/2026-08-09T0124Z-final-review/prepare-fixtures.sh
for tool in mkfs.erofs fsck.erofs dump.erofs python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing G3 equivalence tool: $tool"
done
test "$(id -u)" -eq 0 || \
pre15_infra_blocked 'root is required for deterministic device-node fixtures'
python3 - "$spec" "$PRE15_DUT" <<'PY'
import hashlib
import json
from pathlib import Path
import sys
spec = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
dut = Path(sys.argv[2])
for item in spec["archives"]:
path = dut / item["path"]
data = path.read_bytes()
digest = hashlib.sha256(data).hexdigest()
lines = len(data.splitlines())
if digest != item["sha256"] or lines != item["lines"]:
raise SystemExit(
f"{item['id']}: hash/line mismatch {digest}/{lines}, "
f"expected {item['sha256']}/{item['lines']}"
)
PY
pre15_record_fixture g3-archive-spec "$spec"
pre15_record_fixture g3-stable-helper "$helper"
pre15_record_fixture g3-archive-metadata "$metadata_archive"
pre15_record_fixture g3-archive-final "$final_archive"
mkdir "$PRE15_CASE_TMP/archive-metadata-source" "$PRE15_CASE_TMP/archive-metadata"
rmdir "$PRE15_CASE_TMP/archive-metadata-source" "$PRE15_CASE_TMP/archive-metadata"
FIXTURE_DIR="$PRE15_CASE_TMP/archive-metadata-source" \
ARTIFACT_DIR="$PRE15_CASE_TMP/archive-metadata" "$metadata_archive"
find "$PRE15_CASE_TMP/archive-metadata-source/namei/wide" \
-mindepth 1 -maxdepth 1 -type f -printf '%f\n' | LC_ALL=C sort \
> "$PRE15_CASE_TMP/archive-metadata/expected-wide.txt"
python3 -B "$helper" make-metadata \
--source "$PRE15_CASE_TMP/stable-a-metadata-source" \
--output "$PRE15_CASE_TMP/stable-a-metadata"
python3 -B "$helper" make-metadata \
--source "$PRE15_CASE_TMP/stable-b-metadata-source" \
--output "$PRE15_CASE_TMP/stable-b-metadata"
mkdir -p "$PRE15_RUN_DIR/artifacts"
python3 -B "$helper" compare-set \
--left-source "$PRE15_CASE_TMP/archive-metadata-source" \
--left-artifacts "$PRE15_CASE_TMP/archive-metadata" \
--right-source "$PRE15_CASE_TMP/stable-a-metadata-source" \
--right-artifacts "$PRE15_CASE_TMP/stable-a-metadata" \
--output "$PRE15_RUN_DIR/artifacts/metadata-archive-equivalence.json"
python3 -B "$helper" compare-set \
--left-source "$PRE15_CASE_TMP/stable-a-metadata-source" \
--left-artifacts "$PRE15_CASE_TMP/stable-a-metadata" \
--right-source "$PRE15_CASE_TMP/stable-b-metadata-source" \
--right-artifacts "$PRE15_CASE_TMP/stable-b-metadata" \
--output "$PRE15_RUN_DIR/artifacts/metadata-repeat-equivalence.json"
FIXTURE_DIR="$PRE15_CASE_TMP/archive-final-source" \
ARTIFACT_DIR="$PRE15_CASE_TMP/archive-final" "$final_archive"
python3 -B "$helper" make-final \
--source "$PRE15_CASE_TMP/stable-a-final-source" \
--output "$PRE15_CASE_TMP/stable-a-final"
python3 -B "$helper" make-final \
--source "$PRE15_CASE_TMP/stable-b-final-source" \
--output "$PRE15_CASE_TMP/stable-b-final"
python3 -B "$helper" compare-set \
--left-source "$PRE15_CASE_TMP/archive-final-source" \
--left-artifacts "$PRE15_CASE_TMP/archive-final" \
--right-source "$PRE15_CASE_TMP/stable-a-final-source" \
--right-artifacts "$PRE15_CASE_TMP/stable-a-final" \
--output "$PRE15_RUN_DIR/artifacts/final-archive-equivalence.json"
python3 -B "$helper" compare-set \
--left-source "$PRE15_CASE_TMP/stable-a-final-source" \
--left-artifacts "$PRE15_CASE_TMP/stable-a-final" \
--right-source "$PRE15_CASE_TMP/stable-b-final-source" \
--right-artifacts "$PRE15_CASE_TMP/stable-b-final" \
--output "$PRE15_RUN_DIR/artifacts/final-repeat-equivalence.json"
for evidence in \
metadata-archive-equivalence.json metadata-repeat-equivalence.json \
final-archive-equivalence.json final-repeat-equivalence.json; do
pre15_record_fixture "g3-$evidence" "$PRE15_RUN_DIR/artifacts/$evidence"
done
python3 - "$PRE15_RUN_DIR/artifacts" <<'PY'
import json
from pathlib import Path
import sys
root = Path(sys.argv[1])
names = (
"metadata-archive-equivalence.json",
"metadata-repeat-equivalence.json",
"final-archive-equivalence.json",
"final-repeat-equivalence.json",
)
for name in names:
result = json.loads((root / name).read_text(encoding="ascii"))
if result["status"] != "PASS":
raise SystemExit(f"{name}: {result['status']}")
print("G3 archived=548 lines; source/artifact bytes and expectations are equivalent")
PY
pre15_target_reached
+85
View File
@@ -0,0 +1,85 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
controls=$PRE15_DUT/tests/pre15/fixtures/B01-runner-controls.json
pre15_record_fixture runner-control-contract "$controls"
pre15_record_fixture evidence-schema "$PRE15_DUT/tests/pre15/EVIDENCE-SCHEMA.json"
command -v qemu-system-x86_64 >/dev/null 2>&1 || \
pre15_infra_blocked 'qemu-system-x86_64 is required for the early-exit control'
command -v ssh >/dev/null 2>&1 || \
pre15_infra_blocked 'ssh is required for the connection-failure control'
pre15_control_run "$PRE15_CASE_TMP/known-good" 5 \
sh -c 'printf "reached\n" > "$PRE15_TARGET_MARKER_FILE"'
pre15_control_run "$PRE15_CASE_TMP/dut-mismatch" 5 \
sh -c 'printf "reached\n" > "$PRE15_TARGET_MARKER_FILE"; exit 10'
pre15_control_run "$PRE15_CASE_TMP/guest-command-failure" 5 sh -c 'exit 1'
pre15_control_run "$PRE15_CASE_TMP/ssh-failure" 5 sh -c \
'if ssh -o BatchMode=yes -o ConnectTimeout=1 -p 1 [email protected] true >/dev/null 2>&1; then exit 20; fi; exit 21'
pre15_control_run "$PRE15_CASE_TMP/qemu-early-exit" 5 sh -c \
'qemu-system-x86_64 --pre15-invalid-option >/dev/null 2>&1; qemu_rc=$?; test "$qemu_rc" -ne 0 || exit 20; exit 21'
timeout_pid_file=$PRE15_CASE_TMP/timeout.pid
export timeout_pid_file
pre15_control_run "$PRE15_CASE_TMP/timeout" 1 sh -c \
'printf "%s\n" "$$" > "$timeout_pid_file"; exec sleep 30'
timeout_pid=$(cat "$timeout_pid_file")
if kill -0 "$timeout_pid" 2>/dev/null; then
pre15_runner_fail "timeout control left PID $timeout_pid alive"
fi
pre15_control_run "$PRE15_CASE_TMP/owned-cleanup" 5 sh -c '
sleep 30 >/dev/null 2>&1 &
owned_pid=$!
printf "pid\t%s\tselftest-owned-sleep\n" "$owned_pid" >> "$PRE15_OWNERSHIP_FILE"
printf "%s\n" "$owned_pid" > "$PRE15_RUN_DIR/owned.pid"
printf "reached\n" > "$PRE15_TARGET_MARKER_FILE"
'
owned_pid=$(cat "$PRE15_CASE_TMP/owned-cleanup/owned.pid")
if kill -0 "$owned_pid" 2>/dev/null; then
pre15_runner_fail "owned cleanup left PID $owned_pid alive"
fi
outside_path=$PRE15_CASE_TMP/cleanup-must-refuse
printf 'owned by outer selftest\n' > "$outside_path"
export outside_path
pre15_control_run "$PRE15_CASE_TMP/cleanup-failure" 5 sh -c '
printf "path\t%s\toutside-control-boundary\n" "$outside_path" >> "$PRE15_OWNERSHIP_FILE"
printf "reached\n" > "$PRE15_TARGET_MARKER_FILE"
'
test -f "$outside_path" || \
pre15_runner_fail 'cleanup safety control deleted a non-owned path'
python3 - "$controls" "$PRE15_CASE_TMP" <<'PY'
import json
from pathlib import Path
import sys
contract = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))["controls"]
root = Path(sys.argv[2])
for name, expected in contract.items():
actual = (root / name / "result.tsv").read_text(encoding="ascii").rstrip("\n").split("\t")
if actual != expected:
raise SystemExit(f"{name}: {actual!r}, expected {expected!r}")
print(f"{name}: status={actual[0]} origin={actual[1]} cleanup={actual[2]} timeout={actual[3]}")
PY
mkdir -p "$PRE15_RUN_DIR/artifacts/controls"
for control in \
cleanup-failure dut-mismatch guest-command-failure known-good \
owned-cleanup qemu-early-exit ssh-failure timeout; do
mkdir "$PRE15_RUN_DIR/artifacts/controls/$control"
cp "$PRE15_CASE_TMP/$control/result.tsv" \
"$PRE15_CASE_TMP/$control/stdout.log" \
"$PRE15_CASE_TMP/$control/stderr.log" \
"$PRE15_CASE_TMP/$control/cleanup.log" \
"$PRE15_RUN_DIR/artifacts/controls/$control/"
done
pre15_target_reached
+319
View File
@@ -0,0 +1,319 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_FREEBSD_SRC:?PRE15_FREEBSD_SRC is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
probe=$PRE15_DUT/tests/pre15/probes/ondisk_layout.c
freebsd_header=$PRE15_DUT/src/erofs_fs.h
internal_header=$PRE15_DUT/src/internal.h
linux_header=$PRE15_ROOT/src-linux/erofs_fs.h
sys=$PRE15_FREEBSD_SRC/sys
target=x86_64-unknown-freebsd15.0
artifacts=$PRE15_RUN_DIR/artifacts
include_root=$PRE15_CASE_TMP/include
for tool in clang python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B02 host tool: $tool"
done
test -d "$sys/amd64/include" || \
pre15_infra_blocked "FreeBSD amd64 headers are absent: $sys"
pre15_record_fixture b02-layout-probe "$probe"
pre15_record_fixture b02-freebsd-ondisk-header "$freebsd_header"
pre15_record_fixture b02-freebsd-internal-header "$internal_header"
pre15_record_fixture b02-linux-ondisk-header "$linux_header"
mkdir -p "$artifacts" "$include_root"
ln -s "$sys/amd64/include" "$include_root/machine"
ln -s "$sys/x86/include" "$include_root/x86"
resource_include=$(clang -print-resource-dir)/include
pre15_target_reached
if clang --target="$target" -fsyntax-only -std=gnu17 -Wall -Wextra -Werror \
-nostdinc -isystem "$resource_include" \
-isystem "$PRE15_FREEBSD_SRC/include" -isystem "$include_root" \
-isystem "$sys" -Xclang -fdump-record-layouts "$probe" \
> "$artifacts/freebsd-record-layouts.txt" \
2> "$artifacts/freebsd-layout.stderr"; then
:
else
pre15_dut_fail 'FreeBSD ondisk layout/type/macro oracle failed'
fi
if clang -DB02_LINUX_REFERENCE -fsyntax-only -std=gnu17 \
-Wall -Wextra -Werror -Xclang -fdump-record-layouts "$probe" \
> "$artifacts/linux-record-layouts.txt" \
2> "$artifacts/linux-layout.stderr"; then
:
else
pre15_dut_fail 'Linux reference layout/type/macro oracle failed'
fi
clang --target="$target" -E -dM -std=gnu17 -nostdinc \
-isystem "$resource_include" -isystem "$PRE15_FREEBSD_SRC/include" \
-isystem "$include_root" -isystem "$sys" "$probe" | \
awk '$2 ~ /^(EROFS_|Z_EROFS_)/ { print }' | LC_ALL=C sort \
> "$artifacts/freebsd-macros.txt"
clang -DB02_LINUX_REFERENCE -E -dM -std=gnu17 "$probe" | \
awk '$2 ~ /^(EROFS_|Z_EROFS_)/ { print }' | LC_ALL=C sort \
> "$artifacts/linux-macros.txt"
if python3 - "$PRE15_ROOT" "$artifacts/static-check.json" <<'PY'
from __future__ import annotations
import json
from pathlib import Path
import re
import sys
root = Path(sys.argv[1])
output = Path(sys.argv[2])
dut = root / "repo-pre-15"
src = dut / "src"
ondisk = (src / "erofs_fs.h").read_text(encoding="utf-8")
internal = (src / "internal.h").read_text(encoding="utf-8")
linux = (root / "src-linux/erofs_fs.h").read_text(encoding="utf-8")
def require_once(source: str, token: str) -> None:
count = source.count(token)
if count != 1:
raise SystemExit(f"expected one occurrence of {token!r}, found {count}")
def require_order(source: str, tokens: tuple[str, ...], label: str) -> None:
positions = []
for token in tokens:
require_once(source, token)
positions.append(source.index(token))
if positions != sorted(positions):
raise SystemExit(f"{label} section order mismatch: {tokens!r}")
ondisk_markers = (
"struct erofs_deviceslot {",
"struct erofs_super_block {",
"EROFS_INODE_CHUNK_BASED",
"#define EROFS_CHUNK_FORMAT_BLKBITS_MASK",
"#define EROFS_INODE_LAYOUT_COMPACT",
"struct erofs_inode_chunk_info {",
"struct erofs_inode_compact {",
"struct erofs_inode_extended {",
"struct erofs_xattr_ibody_header {",
"struct erofs_xattr_entry {",
"struct erofs_xattr_long_prefix {",
"#define EROFS_NULL_ADDR",
"struct erofs_inode_chunk_index {",
"#define EROFS_DIRENT_NID_METABOX_BIT",
"struct erofs_dirent {",
"#define EROFS_NAME_LEN",
"struct z_erofs_lz4_cfgs {",
"struct z_erofs_map_header {",
"Z_EROFS_LCLUSTER_TYPE_PLAIN",
"struct z_erofs_lcluster_index {",
"struct z_erofs_extent {",
)
require_order(ondisk, ondisk_markers, "FreeBSD ondisk")
require_order(linux, ondisk_markers, "Linux ondisk")
aliases = {
"EROFS_INODE_LAYOUT_COMPACT": "0",
"EROFS_INODE_LAYOUT_EXTENDED": "1",
"EROFS_INODE_LAYOUT_PLAIN": "EROFS_INODE_FLAT_PLAIN",
}
for name, value in aliases.items():
pattern = rf"^#define[ \t]+{name}[ \t]+{value}$"
if len(re.findall(pattern, ondisk, re.MULTILINE)) != 1:
raise SystemExit(f"layout alias mismatch: {name}")
if not re.search(
r"^#define[ \t]+EROFS_DEVT_SLOT_SIZE[ \t]+"
r"sizeof\(struct erofs_deviceslot\)$",
ondisk,
re.MULTILINE,
):
raise SystemExit("EROFS_DEVT_SLOT_SIZE is not derived from its record")
if re.search(r"^#if.*EROFS_DEVT_SLOT_SIZE", ondisk, re.MULTILINE):
raise SystemExit("sizeof-based EROFS_DEVT_SLOT_SIZE is used in #if")
internal_markers = (
"struct erofs_mount_opts {",
"struct erofs_sb_lz4_info {",
"struct erofs_device_info {",
"struct erofs_xattr_prefix_item {",
"struct erofs_zextent_cache {",
"struct erofs_mount {",
"#define EROFS_FEATURE_FUNCS",
"EROFS_FEATURE_FUNCS(lz4_0padding",
"struct erofs_node {",
"struct erofs_fid {",
"erofs_inode_version(unsigned int ifmt)",
"#define EROFS_MAP_MAPPED",
"struct erofs_map_blocks {",
"struct erofs_map_dev {",
"/* Buffer and device I/O. */",
"/* Logical mapping and file data. */",
"/* Inode and vnode lifecycle. */",
"/* Directory operations. */",
"/* Compressed mapping and data. */",
"/* Compression configuration. */",
"/* VOP vectors. */",
)
require_order(internal, internal_markers, "FreeBSD internal")
if re.search(r"return[ \t]+-E[A-Z0-9_]+", internal):
raise SystemExit("Linux negative-errno convention entered FreeBSD internal.h")
vnops = (src / "erofs_vnops.c").read_text(encoding="utf-8")
super_source = (src / "super.c").read_text(encoding="utf-8")
inode = (src / "inode.c").read_text(encoding="utf-8")
def initializer(source: str, declaration: str, prefix: str) -> list[tuple[str, str]]:
match = re.search(re.escape(declaration) + r"\s*=\s*\{(.*?)\n\};", source, re.DOTALL)
if not match:
raise SystemExit(f"initializer absent: {declaration}")
return re.findall(
rf"^\s*\.({prefix}_[A-Za-z0-9_]+)\s*=\s*([^,]+),",
match.group(1),
re.MULTILINE,
)
vnode = initializer(vnops, "struct vop_vector erofs_vnodeops", "vop")
fifo = initializer(vnops, "struct vop_vector erofs_fifoops", "vop")
vfs = initializer(super_source, "static struct vfsops erofs_vfsops", "vfs")
expected_vnode = [
("vop_default", "&default_vnodeops"),
("vop_inactive", "erofs_inactive"),
("vop_reclaim", "erofs_reclaim"),
("vop_lookup", "vfs_cache_lookup"),
("vop_cachedlookup", "erofs_lookup"),
("vop_readdir", "erofs_readdir"),
("vop_readlink", "erofs_readlink"),
("vop_open", "erofs_open"),
("vop_read", "erofs_read"),
("vop_bmap", "erofs_bmap"),
("vop_getpages", "vnode_pager_local_getpages"),
("vop_getpages_async", "vnode_pager_local_getpages_async"),
("vop_getattr", "erofs_getattr"),
("vop_setattr", "erofs_setattr"),
("vop_access", "erofs_access"),
("vop_pathconf", "erofs_pathconf"),
("vop_getextattr", "erofs_getextattr"),
("vop_listextattr", "erofs_listextattr"),
("vop_deleteextattr", "erofs_deleteextattr"),
("vop_setextattr", "erofs_setextattr"),
("vop_getacl", "erofs_vop_getacl"),
("vop_aclcheck", "erofs_aclcheck"),
("vop_setacl", "erofs_setacl"),
("vop_vptofh", "erofs_vptofh"),
]
expected_fifo = [
("vop_default", "&fifo_specops"),
("vop_access", "erofs_access"),
("vop_aclcheck", "erofs_aclcheck"),
("vop_deleteextattr", "erofs_deleteextattr"),
("vop_getacl", "erofs_vop_getacl"),
("vop_getextattr", "erofs_getextattr"),
("vop_getattr", "erofs_getattr"),
("vop_listextattr", "erofs_listextattr"),
("vop_pathconf", "erofs_pathconf"),
("vop_reclaim", "erofs_reclaim"),
("vop_setacl", "erofs_setacl"),
("vop_setattr", "erofs_setattr"),
("vop_setextattr", "erofs_setextattr"),
("vop_vptofh", "erofs_vptofh"),
]
expected_vfs = [
("vfs_fhtovp", "erofs_fhtovp"),
("vfs_mount", "erofs_mount"),
("vfs_root", "erofs_root"),
("vfs_statfs", "erofs_statfs"),
("vfs_unmount", "erofs_unmount"),
("vfs_vget", "erofs_vgetf"),
]
if vnode != expected_vnode or fifo != expected_fifo or vfs != expected_vfs:
raise SystemExit("VOP/VFS callback targets changed")
hash_calls = re.findall(
r"vfs_hash_(?:get|insert)\s*\([^;]*?erofs_vfs_hash_cmp", inode, re.DOTALL
)
if len(hash_calls) != 2:
raise SystemExit(f"hash comparator callsites changed: {len(hash_calls)}")
decompressors = []
for path in sorted(src.glob("decompressor*.c")):
for line in path.read_text(encoding="utf-8").splitlines():
match = re.match(r"\s*\.(config|decompress)\s*=\s*([^,]+),", line)
if match:
decompressors.append((path.name, match.group(1), match.group(2)))
expected_decompressors = [
("decompressor.c", "decompress", "z_erofs_transform_plain"),
("decompressor.c", "decompress", "z_erofs_transform_plain"),
("decompressor.c", "config", "z_erofs_load_lz4_config"),
("decompressor.c", "decompress", "z_erofs_lz4_decompress"),
("decompressor_deflate.c", "config", "z_erofs_load_deflate_config"),
("decompressor_deflate.c", "decompress", "z_erofs_deflate_decompress"),
("decompressor_lzma.c", "config", "z_erofs_load_lzma_config"),
("decompressor_lzma.c", "decompress", "z_erofs_lzma_decompress"),
("decompressor_zstd.c", "config", "z_erofs_load_zstd_config"),
("decompressor_zstd.c", "decompress", "z_erofs_zstd_decompress"),
]
if decompressors != expected_decompressors:
raise SystemExit("decompressor callback targets changed")
result = {
"status": "PASS",
"source_symbol_denominator": {
"baseline_physical": 464,
"baseline_unique": 389,
"b02_macro_delta": 3,
"expected_physical": 467,
"expected_unique": 392,
"added_names": sorted(aliases),
"kld_symbol_delta": 0,
},
"callbacks": {
"vnode_slots": len(vnode),
"fifo_slots": len(fifo),
"vfs_slots": len(vfs),
"hash_comparator_callsites": len(hash_calls),
"decompressor_fields": len(decompressors),
},
"freebsd_adaptations": [
"packed ondisk records",
"little-endian typedefs",
"explicit supported incompat mask",
"FreeBSD VOP/VFS types",
"positive errno convention",
],
}
with output.open("x", encoding="ascii") as stream:
json.dump(result, stream, ensure_ascii=True, indent=2, sort_keys=True)
stream.write("\n")
print(
"static PASS source-symbols=464+3 callbacks=24+14/6 hash=2 decompress=10"
)
PY
then
:
else
pre15_dut_fail 'header order, symbol denominator, or callback oracle failed'
fi
sha256sum \
"$artifacts/freebsd-record-layouts.txt" \
"$artifacts/linux-record-layouts.txt" \
"$artifacts/freebsd-macros.txt" \
"$artifacts/linux-macros.txt" \
"$artifacts/static-check.json" \
> "$artifacts/SHA256SUMS"
printf 'layout PASS FreeBSD and Linux size/offset/alignment/type/macro oracles\n'
+190
View File
@@ -0,0 +1,190 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
internal=$PRE15_DUT/src/internal.h
data=$PRE15_DUT/src/data.c
inode=$PRE15_DUT/src/inode.c
super=$PRE15_DUT/src/super.c
zmap=$PRE15_DUT/src/zmap.c
linux_internal=$PRE15_ROOT/src-linux/internal.h
artifacts=$PRE15_RUN_DIR/artifacts
for tool in python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B05 host tool: $tool"
done
pre15_record_fixture b05-freebsd-internal "$internal"
pre15_record_fixture b05-freebsd-data "$data"
pre15_record_fixture b05-freebsd-inode "$inode"
pre15_record_fixture b05-freebsd-super "$super"
pre15_record_fixture b05-freebsd-zmap "$zmap"
pre15_record_fixture b05-linux-internal "$linux_internal"
mkdir -p "$artifacts"
pre15_target_reached
if python3 - "$PRE15_ROOT" "$artifacts/field-check.json" <<'PY'
from __future__ import annotations
import json
from pathlib import Path
import re
import sys
root = Path(sys.argv[1])
output = Path(sys.argv[2])
dut_src = root / "repo-pre-15" / "src"
internal = (dut_src / "internal.h").read_text(encoding="utf-8")
data = (dut_src / "data.c").read_text(encoding="utf-8")
inode = (dut_src / "inode.c").read_text(encoding="utf-8")
super_source = (dut_src / "super.c").read_text(encoding="utf-8")
zmap = (dut_src / "zmap.c").read_text(encoding="utf-8")
linux = (root / "src-linux" / "internal.h").read_text(encoding="utf-8")
def struct_body(source: str, name: str) -> str:
match = re.search(
rf"^struct {re.escape(name)} \{{\n(?P<body>.*?)^\}};",
source,
re.MULTILINE | re.DOTALL,
)
if not match:
raise SystemExit(f"missing struct {name}")
return match.group("body")
def fields(source: str, name: str) -> list[str]:
result = []
for line in struct_body(source, name).splitlines():
declaration = line.strip()
if not declaration or declaration.startswith("/*"):
continue
match = re.search(r"([A-Za-z_][A-Za-z0-9_]*)\s*(?:\[[^]]+\])?;$", declaration)
if not match:
raise SystemExit(f"unparsed field in struct {name}: {declaration!r}")
result.append(match.group(1))
return result
freebsd_device = fields(internal, "erofs_device_info")
linux_device = fields(linux, "erofs_device_info")
expected_device = [
"devvp",
"dev",
"cp",
"mediasize",
"sectorsize",
"blocks",
"uniaddr",
]
if freebsd_device != expected_device:
raise SystemExit(f"FreeBSD device field order mismatch: {freebsd_device!r}")
if [field for field in linux_device if field in {"blocks", "uniaddr"}] != [
"blocks",
"uniaddr",
]:
raise SystemExit(f"Linux device common-field order mismatch: {linux_device!r}")
freebsd_map = fields(internal, "erofs_map_dev")
linux_map = fields(linux, "erofs_map_dev")
expected_map = ["m_dif", "m_pa", "m_deviceid", "m_plen"]
if freebsd_map != expected_map:
raise SystemExit(f"FreeBSD map field order mismatch: {freebsd_map!r}")
if [field for field in linux_map if field in {"m_dif", "m_pa", "m_deviceid"}] != [
"m_dif",
"m_pa",
"m_deviceid",
]:
raise SystemExit(f"Linux map common-field order mismatch: {linux_map!r}")
inode_fields = fields(internal, "erofs_inode")
for removed in ("ino", "inline_data"):
if removed in inode_fields:
raise SystemExit(f"removed inode field remains: {removed}")
if "m_sbi" in freebsd_map:
raise SystemExit("removed map back-pointer remains")
removed_uses = {
"map back-pointer": sum(
source.count("m_sbi") for source in (internal, data, inode, super_source, zmap)
),
"saved ondisk ino": len(re.findall(r"\bvi->ino\b", inode)),
"inline-data cache": len(re.findall(r"\bvi->inline_data\b", inode)),
}
if any(removed_uses.values()):
raise SystemExit(f"removed field use remains: {removed_uses!r}")
helper = re.search(
r"erofs_fill_from_devinfo\(struct erofs_map_dev \*map,\n"
r" struct erofs_device_info \*dif, erofs_off_t pa\)",
data,
)
if not helper:
raise SystemExit("map helper still accepts the removed super back-pointer")
if len(re.findall(r"erofs_fill_from_devinfo\(", data)) != 4:
raise SystemExit("unexpected map helper definition/call count")
initializer = re.search(
r"map = \(struct erofs_map_dev\) \{\n"
r"\t\t\.m_pa = off,\n"
r"\t\t\.m_deviceid = device_id,\n"
r"\t\t\.m_plen = len,\n"
r"\t\};",
data,
)
if not initializer:
raise SystemExit("erofs_map_dev initializer order mismatch")
required_markers = {
"raw nid identity": "vi->nid = nid;",
"layout source": "vi->datalayout = erofs_inode_datalayout(ifmt);",
"device vnode": "struct vnode *devvp;",
"GEOM consumer": "struct g_consumer *cp;",
"provider media size": "uint64_t mediasize;",
"provider sector size": "uint32_t sectorsize;",
"future volume label": "char volume_name[17];",
"deferred LZ4 state": "struct erofs_sb_lz4_info lz4;",
}
for label, marker in required_markers.items():
if marker not in internal and marker not in inode:
raise SystemExit(f"required ownership/source marker missing: {label}")
result = {
"freebsd_device_fields": freebsd_device,
"freebsd_map_fields": freebsd_map,
"linux_device_common_fields": [
field for field in linux_device if field in {"blocks", "uniaddr"}
],
"linux_map_common_fields": [
field for field in linux_map if field in {"m_dif", "m_pa", "m_deviceid"}
],
"removed_field_uses": removed_uses,
"retained_freebsd_ownership_fields": [
"devvp",
"dev",
"cp",
"mediasize",
"sectorsize",
],
}
with output.open("x", encoding="ascii") as stream:
json.dump(result, stream, ensure_ascii=True, indent=2, sort_keys=True)
stream.write("\n")
print("fields PASS device=7 map=4 removed-uses=0")
PY
then
:
else
pre15_dut_fail 'B05 field layout or write-only closure oracle failed'
fi
sha256sum "$artifacts/field-check.json" > "$artifacts/SHA256SUMS"
printf 'B05 PASS private field model and zero-consumer closure\n'
+198
View File
@@ -0,0 +1,198 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
gate=$PRE15_DUT/tests/pre15/gates/P15-005.sh
tuple_oracle=$PRE15_DUT/tests/pre15/fixtures/B06-tuples.json
ownership_oracle=$PRE15_DUT/tests/pre15/fixtures/B06-ownership.json
artifacts=$PRE15_RUN_DIR/artifacts
for tool in cc python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B06 host tool: $tool"
done
pre15_record_fixture b06-gate "$gate"
pre15_record_fixture b06-tuple-oracle "$tuple_oracle"
pre15_record_fixture b06-ownership-oracle "$ownership_oracle"
for source in internal.h data.c decompressor.c inode.c super.c xattr.c zdata.c zmap.c; do
pre15_record_fixture "b06-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
pre15_target_reached
if "$gate" --worktree --oracle "$tuple_oracle" \
--output "$artifacts/gate" >"$artifacts/gate.stdout" \
2>"$artifacts/gate.stderr"; then
:
else
pre15_dut_fail 'B06 G02 candidate replay failed'
fi
if python3 - "$PRE15_ROOT" "$ownership_oracle" "$artifacts/gate" \
"$artifacts/ownership-check.json" <<'PY'
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import re
import sys
root = Path(sys.argv[1])
expected = json.loads(Path(sys.argv[2]).read_text(encoding="ascii"))
gate_dir = Path(sys.argv[3])
output = Path(sys.argv[4])
result = json.loads((gate_dir / "result.json").read_text(encoding="ascii"))
ownership = json.loads((gate_dir / "ownership.json").read_text(encoding="ascii"))
if result["status"] != "GO" or result["tuple_status"] != "PASS":
raise SystemExit(f"candidate tuple result is not GO: {result!r}")
if not result["object_mode"] or result["oracle_equal"] is not True:
raise SystemExit(f"candidate did not replay the frozen object oracle: {result!r}")
if ownership["status"] != "PASS" or ownership["failures"]:
raise SystemExit(f"candidate ownership result failed: {ownership['failures']!r}")
if ownership["consumers"] != expected["consumers"]:
raise SystemExit("ownership consumer set changed")
if ownership["direct_metadata_functions"] != expected["direct_metadata_functions"]:
raise SystemExit("direct metadata consumer inventory changed")
if ownership["helper_paths"]["paths"] != expected["helper_paths"]:
raise SystemExit("actual helper lifecycle paths changed")
metric_order = expected["metric_order"]
actual_metrics = {
item["function"]: [item[field] for field in metric_order]
for item in ownership["candidate_contract"]["function_metrics"]
}
if actual_metrics != expected["function_metrics"]:
raise SystemExit("per-function ownership metrics changed")
def extract_function(source: str, name: str) -> str:
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if not match:
raise SystemExit(f"backend function missing: {name}")
start = source.rfind("\n\n", 0, match.start()) + 2
brace = source.find("{", match.end())
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1] + "\n"
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
raise SystemExit(f"unterminated backend function: {name}")
backend_hashes = {}
for key, frozen_hash in expected["freebsd_backend_function_sha256"].items():
filename, function = key.split(":", 1)
source = (root / "repo-pre-15" / "src" / filename).read_text(encoding="utf-8")
current_hash = hashlib.sha256(
extract_function(source, function).encode("utf-8")
).hexdigest()
backend_hashes[key] = current_hash
if current_hash != frozen_hash:
raise SystemExit(f"FreeBSD backend lifecycle changed: {key}")
internal_path = root / "repo-pre-15" / "src" / "internal.h"
internal = internal_path.read_text(encoding="utf-8")
match = re.search(
r"^struct erofs_map_blocks \{\n.*?^\};\n",
internal,
re.MULTILINE | re.DOTALL,
)
if not match:
raise SystemExit("missing legacy map tuple object")
map_hash = hashlib.sha256(match.group(0).encode("utf-8")).hexdigest()
if map_hash != expected["map_blocks_sha256"]:
raise SystemExit("B07 map objectization leaked into B06")
required = [
"struct erofs_buf {",
"#define EROFS_BUF_INITIALIZER { .data = NULL, .release = NULL }",
"void erofs_put_metabuf(struct erofs_buf *buf);",
"int erofs_bread(struct erofs_sb_info *sbi, erofs_off_t off, size_t len, void **bufp);",
"void erofs_brelse(void *buf);",
]
for marker in required:
if marker not in internal:
raise SystemExit(f"required B06/raw API marker missing: {marker}")
if re.search(
r"erofs_read_metadata\s*\([^;]*void\s*\*\*bufp\s*\)\s*;",
internal,
re.MULTILINE | re.DOTALL,
):
raise SystemExit("legacy metadata void-pointer API remains")
for filename in ("dir.c", "namei.c"):
text = (root / "repo-pre-15" / "src" / filename).read_text(encoding="utf-8")
if "struct erofs_buf" in text or "erofs_put_metabuf" in text:
raise SystemExit(f"raw directory consumer was objectized: {filename}")
summary = {
"status": "PASS",
"tuple_cases": result["case_count"],
"tuple_oracle_equal": result["oracle_equal"],
"ownership_paths": result["ownership_path_count"],
"helper_paths": len(expected["helper_paths"]),
"function_metrics": len(actual_metrics),
"freebsd_backend_functions": len(backend_hashes),
"map_blocks_sha256": map_hash,
"qemu": result["qemu"],
"full_feature_suite": result["full_feature_suite"],
}
with output.open("x", encoding="ascii") as stream:
json.dump(summary, stream, ensure_ascii=True, indent=2, sort_keys=True)
stream.write("\n")
print("B06 ownership PASS tuples=14 paths=22 helper=5 functions=28")
PY
then
:
else
pre15_dut_fail 'B06 ownership fixture comparison failed'
fi
sha256sum "$artifacts/gate/result.json" "$artifacts/gate/tuples.json" \
"$artifacts/gate/ownership.json" "$artifacts/ownership-check.json" \
>"$artifacts/SHA256SUMS"
printf 'B06 PASS explicit metadata ownership and frozen tuple replay\n'
+198
View File
@@ -0,0 +1,198 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
gate=$PRE15_DUT/tests/pre15/gates/P15-006.sh
input=$PRE15_DUT/tests/pre15/gates/P15-006-input.json
oracle=$PRE15_DUT/tests/pre15/fixtures/B07-map-oracle.json
artifacts=$PRE15_RUN_DIR/artifacts
for tool in cc python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B07a host tool: $tool"
done
pre15_record_fixture b07a-gate "$gate"
pre15_record_fixture b07a-input "$input"
pre15_record_fixture b07a-oracle "$oracle"
for source in internal.h data.c zmap.c zdata.c super.c; do
pre15_record_fixture "b07a-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
pre15_target_reached
if "$gate" --worktree --oracle "$oracle" --output "$artifacts/gate" \
>"$artifacts/gate.stdout" 2>"$artifacts/gate.stderr"; then
:
else
pre15_dut_fail 'B07a P15-006 candidate replay failed'
fi
if python3 - "$PRE15_DUT" "$oracle" "$artifacts/gate" \
"$artifacts/adapter-check.json" <<'PY'
from __future__ import annotations
import json
from pathlib import Path
import re
import sys
dut = Path(sys.argv[1])
oracle = json.loads(Path(sys.argv[2]).read_text(encoding="ascii"))
gate = Path(sys.argv[3])
output = Path(sys.argv[4])
result = json.loads((gate / "result.json").read_text(encoding="ascii"))
tuples = json.loads((gate / "tuples.json").read_text(encoding="ascii"))
devices = json.loads((gate / "devices.json").read_text(encoding="ascii"))
if result["status"] != "GO" or result["failures"]:
raise SystemExit(f"P15-006 candidate did not GO: {result!r}")
if not result["adapter_mode"] or result["oracle_equal"] is not True:
raise SystemExit("candidate did not use the common map adapter and frozen oracle")
if result["map_case_count"] != 80 or result["device_case_count"] != 13:
raise SystemExit("P15-006 corpus denominator changed")
if result["model_sha256"] != oracle["model_sha256"]:
raise SystemExit("independent model digest changed")
if result["tuple_bytes_sha256"] != oracle["tuple_bytes_sha256"]:
raise SystemExit("tuple byte stream changed")
if result["adapter_probe"]["status"] != "PASS":
raise SystemExit("FULL/COMPACT adapter pass-through probe failed")
if not result["adapter_probe"]["full_and_compact_dispatch"]:
raise SystemExit("compressed layout dispatch is incomplete")
if result["adapter_probe"]["flag_mask"] != 0x1F:
raise SystemExit("not all map flag bits passed through the adapter")
if tuples["tuple_bytes_sha256"] != oracle["tuple_bytes_sha256"]:
raise SystemExit("tuple artifact does not match frozen bytes")
if devices["status"] != "PASS" or len(devices["records"]) != 13:
raise SystemExit("device/GEOM resolution oracle failed")
def extract_function(source: str, name: str) -> str:
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if not match:
raise SystemExit(f"missing function: {name}")
start = source.rfind("\n\n", 0, match.start()) + 2
brace = source.find("{", match.end())
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
raise SystemExit(f"unterminated function: {name}")
internal = (dut / "src/internal.h").read_text(encoding="utf-8")
data = (dut / "src/data.c").read_text(encoding="utf-8")
zmap = (dut / "src/zmap.c").read_text(encoding="utf-8")
zdata = (dut / "src/zdata.c").read_text(encoding="utf-8")
super_source = (dut / "src/super.c").read_text(encoding="utf-8")
object_prototype = re.search(
r"int erofs_map_blocks\s*\(struct erofs_sb_info \*sbi,\s*"
r"struct erofs_inode \*vi,\s*struct erofs_map_blocks \*map\s*\);",
internal,
re.MULTILINE,
)
if object_prototype is None:
raise SystemExit("common map object prototype is absent")
if re.search(r"erofs_map_blocks\s*\([^;]*phys_off", internal, re.DOTALL):
raise SystemExit("legacy scatter prototype remains exported")
if "static int\nerofs_map_blocks_legacy(" not in data:
raise SystemExit("legacy producer target is not file-local")
if len(re.findall(r"\berofs_map_blocks_legacy\s*\(", data)) != 4:
raise SystemExit("B07a must retain exactly two legacy data consumers")
if len(re.findall(r"\berofs_map_blocks\s*\(", data)) != 1:
raise SystemExit("B07a common map entry has an unexpected data.c caller")
adapter = extract_function(data, "erofs_map_blocks")
required_adapter = [
"struct erofs_map_blocks next = { .m_la = map->m_la };",
"z_erofs_map_blocks_iter(sbi, vi, &next, 0);",
"next.m_deviceid = device_id;",
"next.m_flags |= EROFS_MAP_MAPPED;",
"next.m_flags |= EROFS_MAP_META;",
"*map = next;",
]
for marker in required_adapter:
if marker not in adapter:
raise SystemExit(f"adapter contract marker is absent: {marker}")
for function in ("erofs_read_data", "erofs_read_uio"):
body = extract_function(data, function)
if "erofs_map_blocks_legacy(" not in body or "erofs_map_blocks(" in body:
raise SystemExit(f"B07a migrated consumer early: {function}")
for source_name, source in (("zdata.c", zdata), ("super.c", super_source)):
if "z_erofs_map_blocks_iter(" not in source or "erofs_map_blocks(" in source:
raise SystemExit(f"B07a migrated compressed consumer early: {source_name}")
if "map adapter must preserve every map flag" not in zmap:
raise SystemExit("compressed map flag contract assertion is absent")
summary = {
"status": "PASS",
"map_cases": result["map_case_count"],
"device_cases": result["device_case_count"],
"tuple_bytes_sha256": result["tuple_bytes_sha256"],
"model_sha256": result["model_sha256"],
"adapter_probe": result["adapter_probe"],
"legacy_data_consumers": 2,
"compressed_consumers_migrated": False,
"qemu": result["qemu"],
"full_feature_suite": result["full_feature_suite"],
}
with output.open("x", encoding="ascii") as stream:
json.dump(summary, stream, ensure_ascii=True, indent=2, sort_keys=True)
stream.write("\n")
print("B07a PASS map=80 device=13 tuple-bytes exact flags=0x1f")
PY
then
:
else
pre15_dut_fail 'B07a adapter boundary check failed'
fi
sha256sum "$artifacts/gate/result.json" "$artifacts/gate/tuples.json" \
"$artifacts/gate/devices.json" "$artifacts/adapter-check.json" \
>"$artifacts/SHA256SUMS"
printf 'B07a PASS exact map adapter; producers and consumers remain staged\n'
+360
View File
@@ -0,0 +1,360 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
gate=$PRE15_DUT/tests/pre15/gates/P15-006.sh
input=$PRE15_DUT/tests/pre15/gates/P15-006-input.json
oracle=$PRE15_DUT/tests/pre15/fixtures/B07-map-oracle.json
artifacts=$PRE15_RUN_DIR/artifacts
baseline=6673f51152a5195a8a8903aa801f820abce7936e
b07a=141b11f0d847a63a47b6c6143e4c2913a1147a0f
for tool in cc git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B07b host tool: $tool"
done
pre15_record_fixture b07b-gate "$gate"
pre15_record_fixture b07b-input "$input"
pre15_record_fixture b07b-oracle "$oracle"
for source in internal.h data.c zmap.c zdata.c super.c; do
pre15_record_fixture "b07b-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
pre15_target_reached
if "$gate" --base "$baseline" --oracle "$oracle" \
--output "$artifacts/baseline" >"$artifacts/baseline.stdout" \
2>"$artifacts/baseline.stderr"; then
:
else
pre15_dut_fail 'B07b frozen P15-006 baseline replay failed'
fi
if python3 - "$gate" "$artifacts/P15-006-B07b.py" <<'PY'
from pathlib import Path
import sys
source = Path(sys.argv[1]).read_text(encoding="utf-8")
marker = "<<'PY'\n"
start = source.index(marker) + len(marker)
end = source.rindex("\nPY\n")
program = source[start:end]
def replace_once(old: str, new: str) -> None:
global program
if program.count(old) != 1:
raise SystemExit(f"P15-006 derivation marker count changed: {old[:60]!r}")
program = program.replace(old, new)
replace_once(
''' if actual_hash != expected_hash:
raise SystemExit(f"protected producer/GEOM body changed: {key}")
''',
''' if actual_hash != expected_hash and key != "data.c:erofs_map_blocks_chunk":
raise SystemExit(f"protected producer/GEOM body changed: {key}")
''',
)
replace_once(
'''if candidate_mode:
legacy_body = extract_function(data_source, "erofs_map_blocks_legacy")
expected_legacy_hash = SPEC["candidate_legacy_map_sha256"]
else:
legacy_body = extract_function(data_source, "erofs_map_blocks")
expected_legacy_hash = SPEC["baseline_legacy_map_sha256"]
if sha256_bytes(legacy_body.encode("utf-8")) != expected_legacy_hash:
raise SystemExit("plain/chunk legacy producer body changed")
''',
'''if candidate_mode:
legacy_body = extract_function(data_source, "erofs_map_blocks_legacy")
expected_legacy_hash = None
else:
legacy_body = extract_function(data_source, "erofs_map_blocks")
expected_legacy_hash = SPEC["baseline_legacy_map_sha256"]
if expected_legacy_hash is not None and sha256_bytes(legacy_body.encode("utf-8")) != expected_legacy_hash:
raise SystemExit("plain/chunk legacy producer body changed")
''',
)
replace_once(
'''program += extract_function(data_source, "erofs_inline_tail_start")
program += extract_function(data_source, "erofs_map_blocks_chunk")
program += legacy_body
if candidate_mode:
program += extract_function(data_source, "erofs_map_blocks")
''',
'''program += extract_function(data_source, "erofs_inline_tail_start")
program += extract_function(data_source, "erofs_map_blocks_chunk")
if candidate_mode:
program += extract_function(data_source, "erofs_map_blocks_flatmode")
program += extract_function(data_source, "erofs_map_blocks")
else:
program += legacy_body
''',
)
replace_once(
''' adapter_program += extract_function(data_source, "erofs_inline_tail_start")
adapter_program += extract_function(data_source, "erofs_map_blocks_chunk")
adapter_program += legacy_body
adapter_program += extract_function(data_source, "erofs_map_blocks")
''',
''' adapter_program += extract_function(data_source, "erofs_inline_tail_start")
adapter_program += extract_function(data_source, "erofs_map_blocks_chunk")
adapter_program += extract_function(data_source, "erofs_map_blocks_flatmode")
adapter_program += extract_function(data_source, "erofs_map_blocks")
''',
)
Path(sys.argv[2]).write_text(program + "\n", encoding="ascii")
PY
then
:
else
pre15_dut_fail 'B07b could not derive the current-producer replay'
fi
if python3 "$artifacts/P15-006-B07b.py" "$PRE15_ROOT" "$input" \
worktree '' "$artifacts/candidate" "$oracle" \
>"$artifacts/candidate.stdout" 2>"$artifacts/candidate.stderr"; then
:
else
pre15_dut_fail 'B07b object producer replay failed'
fi
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$oracle" \
"$artifacts/baseline" "$artifacts/candidate" "$artifacts/producer-check.json" \
"$b07a" <<'PY'
from __future__ import annotations
from collections import Counter
import hashlib
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
oracle = json.loads(Path(sys.argv[3]).read_text(encoding="ascii"))
baseline = Path(sys.argv[4])
candidate = Path(sys.argv[5])
output = Path(sys.argv[6])
b07a = sys.argv[7]
def load(directory: Path, name: str) -> dict:
return json.loads((directory / name).read_text(encoding="ascii"))
def extract_function(source: str, name: str) -> str:
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if not match:
raise SystemExit(f"missing function: {name}")
start = source.rfind("\n\n", 0, match.start()) + 2
brace = source.find("{", match.end())
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
raise SystemExit(f"unterminated function: {name}")
def committed_source(path: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{b07a}:repo-pre-15/src/{path}"],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
raise SystemExit(f"cannot read B07a source {path}: {completed.stderr}")
return completed.stdout
base_result = load(baseline, "result.json")
base_tuples = load(baseline, "tuples.json")
base_devices = load(baseline, "devices.json")
result = load(candidate, "result.json")
tuples = load(candidate, "tuples.json")
devices = load(candidate, "devices.json")
for label, record in (("baseline", base_result), ("candidate", result)):
if record["status"] != "GO" or record["failures"]:
raise SystemExit(f"{label} P15-006 replay did not GO: {record!r}")
if base_result["map_case_count"] != 80 or result["map_case_count"] != 80:
raise SystemExit("B07 map denominator changed")
if base_result["device_case_count"] != 13 or result["device_case_count"] != 13:
raise SystemExit("B07 device denominator changed")
if result["oracle_equal"] is not True or base_result["oracle_equal"] is not True:
raise SystemExit("frozen B07 oracle comparison failed")
if result["model_sha256"] != oracle["model_sha256"]:
raise SystemExit("independent P15-006 model digest changed")
if result["tuple_bytes_sha256"] != oracle["tuple_bytes_sha256"]:
raise SystemExit("full tuple stream differs from the frozen B07 oracle")
if result["adapter_probe"]["status"] != "PASS":
raise SystemExit("common adapter probe failed after producer migration")
base_records = {record["id"]: record for record in base_tuples["records"]}
records = {record["id"]: record for record in tuples["records"]}
if set(base_records) != set(records) or len(records) != 80:
raise SystemExit("B07 tuple ID set changed")
full_diffs = [
case_id for case_id in records
if records[case_id]["tuple_hex"] != base_records[case_id]["tuple_hex"]
]
producer_ids = [
record["id"] for record in tuples["records"] if record["engine"] == "data"
]
if len(producer_ids) != 50:
raise SystemExit(f"plain/chunk producer denominator changed: {len(producer_ids)}")
producer_diffs = [case_id for case_id in producer_ids if case_id in full_diffs]
producer_bytes = b"".join(bytes.fromhex(records[case_id]["tuple_hex"]) for case_id in producer_ids)
producer_sha256 = hashlib.sha256(producer_bytes).hexdigest()
if full_diffs or producer_diffs:
raise SystemExit(
f"B07b tuple difference: producer={producer_diffs!r} full={full_diffs!r}"
)
if (
devices["records"] != base_devices["records"]
or devices["status"] != "PASS"
or base_devices["status"] != "PASS"
):
raise SystemExit("B07b device-resolution records changed")
data = (dut / "src/data.c").read_text(encoding="utf-8")
chunk = extract_function(data, "erofs_map_blocks_chunk")
flatmode = extract_function(data, "erofs_map_blocks_flatmode")
common = extract_function(data, "erofs_map_blocks")
legacy = extract_function(data, "erofs_map_blocks_legacy")
if "struct erofs_map_blocks *map" not in chunk.split("{", 1)[0]:
raise SystemExit("chunk producer does not accept the common map object")
for marker in (
"map->m_pa", "map->m_llen", "map->m_plen", "map->m_deviceid",
"map->m_flags |= EROFS_MAP_MAPPED",
):
if marker not in chunk:
raise SystemExit(f"chunk producer field is not object-backed: {marker}")
for marker in (
"map->m_la", "map->m_pa", "map->m_llen", "map->m_plen",
"EROFS_MAP_MAPPED", "EROFS_MAP_META",
):
if marker not in flatmode:
raise SystemExit(f"flat producer field is not object-backed: {marker}")
if "erofs_map_blocks_flatmode(sbi, vi, &next)" not in common:
raise SystemExit("common entry does not call the object flat producer")
if "erofs_map_blocks(sbi, vi, &map)" not in legacy:
raise SystemExit("legacy scatter adapter does not delegate to the object entry")
if len(re.findall(r"\berofs_map_blocks_legacy\s*\(", data)) != 3:
raise SystemExit("B07b must retain exactly two legacy data consumers")
old_data = committed_source("data.c")
for function in ("erofs_read_data", "erofs_read_uio"):
body = extract_function(data, function)
if body != extract_function(old_data, function):
raise SystemExit(f"B07b changed consumer early: {function}")
if "erofs_map_blocks_legacy(" not in body or "erofs_map_blocks(" in body:
raise SystemExit(f"B07b migrated consumer early: {function}")
for filename in ("internal.h", "super.c", "zdata.c", "zmap.c"):
current = (dut / "src" / filename).read_text(encoding="utf-8")
if current != committed_source(filename):
raise SystemExit(f"B07b changed non-producer source: {filename}")
cleanup = Counter(
(record["actual"]["acquire_count"], record["actual"]["release_count"])
for record in tuples["records"]
)
if any(acquire != release for acquire, release in cleanup):
raise SystemExit(f"metadata cleanup became unbalanced: {cleanup!r}")
if any(record["actual"]["errno"] < 0 for record in tuples["records"]):
raise SystemExit("negative errno observed")
summary = {
"status": "PASS",
"final_id": "P15-006",
"map_cases": 80,
"producer_cases": len(producer_ids),
"compressed_control_cases": 30,
"device_cases": 13,
"producer_tuple_diff_count": len(producer_diffs),
"full_tuple_diff_count": len(full_diffs),
"producer_tuple_bytes": len(producer_bytes),
"producer_tuple_bytes_sha256": producer_sha256,
"full_tuple_bytes_sha256": result["tuple_bytes_sha256"],
"model_sha256": result["model_sha256"],
"legacy_data_consumers": 2,
"compressed_consumers_migrated": False,
"cleanup_distribution": {
f"{acquire}:{release}": count
for (acquire, release), count in sorted(cleanup.items())
},
"qemu": "NOT_RUN",
"full_feature_suite": "NOT_RUN",
}
with output.open("x", encoding="ascii") as stream:
json.dump(summary, stream, ensure_ascii=True, indent=2, sort_keys=True)
stream.write("\n")
print(
"B07b PASS producer=50 diff=0 full=80 device=13 "
f"producer-sha256={producer_sha256}"
)
PY
then
:
else
pre15_dut_fail 'B07b producer boundary check failed'
fi
sha256sum "$artifacts/baseline/result.json" \
"$artifacts/baseline/tuples.json" "$artifacts/baseline/devices.json" \
"$artifacts/candidate/result.json" "$artifacts/candidate/tuples.json" \
"$artifacts/candidate/devices.json" "$artifacts/producer-check.json" \
>"$artifacts/SHA256SUMS"
printf 'B07b PASS object producers; all consumers remain staged\n'
+531
View File
@@ -0,0 +1,531 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
gate=$PRE15_DUT/tests/pre15/gates/P15-006.sh
input=$PRE15_DUT/tests/pre15/gates/P15-006-input.json
oracle=$PRE15_DUT/tests/pre15/fixtures/B07-map-oracle.json
artifacts=$PRE15_RUN_DIR/artifacts
baseline=6673f51152a5195a8a8903aa801f820abce7936e
b07b=d58f131cfe855728969aa3695d34a26a61b90277
for tool in cc git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B07c host tool: $tool"
done
pre15_record_fixture b07c-gate "$gate"
pre15_record_fixture b07c-input "$input"
pre15_record_fixture b07c-oracle "$oracle"
for source in internal.h data.c zmap.c zdata.c super.c; do
pre15_record_fixture "b07c-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
pre15_target_reached
if "$gate" --base "$baseline" --oracle "$oracle" \
--output "$artifacts/baseline" >"$artifacts/baseline.stdout" \
2>"$artifacts/baseline.stderr"; then
:
else
pre15_dut_fail 'B07c frozen P15-006 baseline replay failed'
fi
if python3 - "$gate" "$artifacts/P15-006-B07c.py" <<'PY'
from pathlib import Path
import sys
source = Path(sys.argv[1]).read_text(encoding="utf-8")
marker = "<<'PY'\n"
start = source.index(marker) + len(marker)
end = source.rindex("\nPY\n")
program = source[start:end]
def replace_once(old: str, new: str) -> None:
global program
if program.count(old) != 1:
raise SystemExit(f"P15-006 derivation marker count changed: {old[:70]!r}")
program = program.replace(old, new)
replace_once(
'''if candidate_mode and not re.search(
r"^erofs_map_blocks_legacy\\s*\\(", data_source, re.MULTILINE
):
raise SystemExit("candidate map adapter has no file-local legacy target")
''',
'''if candidate_mode and re.search(
r"^erofs_map_blocks_legacy\\s*\\(", data_source, re.MULTILINE
):
raise SystemExit("B07c candidate still has the legacy scatter adapter")
''',
)
replace_once(
'''for key, expected_hash in SPEC["protected_function_sha256"].items():
filename, function = key.split(":", 1)
text = data_source if filename == "data.c" else zmap_source
actual_hash = sha256_bytes(extract_function(text, function).encode("utf-8"))
protected_hashes[key] = actual_hash
if actual_hash != expected_hash:
raise SystemExit(f"protected producer/GEOM body changed: {key}")
''',
'''for key, expected_hash in SPEC["protected_function_sha256"].items():
filename, function = key.split(":", 1)
text = data_source if filename == "data.c" else zmap_source
actual_function = (
"z_erofs_map_blocks"
if key == "zmap.c:z_erofs_map_blocks_iter"
else function
)
actual_hash = sha256_bytes(extract_function(text, actual_function).encode("utf-8"))
protected_hashes[key] = actual_hash
allowed = {
"data.c:erofs_map_blocks_chunk",
"zmap.c:z_erofs_map_blocks_iter",
}
if actual_hash != expected_hash and key not in allowed:
raise SystemExit(f"protected producer/GEOM body changed: {key}")
''',
)
replace_once(
'''if candidate_mode:
legacy_body = extract_function(data_source, "erofs_map_blocks_legacy")
expected_legacy_hash = SPEC["candidate_legacy_map_sha256"]
else:
legacy_body = extract_function(data_source, "erofs_map_blocks")
expected_legacy_hash = SPEC["baseline_legacy_map_sha256"]
if sha256_bytes(legacy_body.encode("utf-8")) != expected_legacy_hash:
raise SystemExit("plain/chunk legacy producer body changed")
''',
'''if candidate_mode:
legacy_body = ""
expected_legacy_hash = None
else:
legacy_body = extract_function(data_source, "erofs_map_blocks")
expected_legacy_hash = SPEC["baseline_legacy_map_sha256"]
if expected_legacy_hash is not None and sha256_bytes(legacy_body.encode("utf-8")) != expected_legacy_hash:
raise SystemExit("plain/chunk legacy producer body changed")
''',
)
replace_once(
' "z_erofs_map_blocks_iter",\n',
' "z_erofs_map_blocks",\n',
)
replace_once(
'program += "int z_erofs_map_blocks_iter(struct erofs_sb_info *, struct erofs_inode *, struct erofs_map_blocks *, int);\\n"\n',
'program += "int z_erofs_map_blocks(struct erofs_sb_info *, struct erofs_inode *, struct erofs_map_blocks *);\\n"\n',
)
replace_once(
'''program += extract_function(data_source, "erofs_inline_tail_start")
program += extract_function(data_source, "erofs_map_blocks_chunk")
program += legacy_body
if candidate_mode:
program += extract_function(data_source, "erofs_map_blocks")
''',
'''program += extract_function(data_source, "erofs_inline_tail_start")
program += extract_function(data_source, "erofs_map_blocks_chunk")
if candidate_mode:
program += extract_function(data_source, "erofs_map_blocks_flatmode")
program += extract_function(data_source, "erofs_map_blocks")
else:
program += legacy_body
''',
)
replace_once(
'''int z_erofs_map_blocks_iter(struct erofs_sb_info *sbi,
struct erofs_inode *vi, struct erofs_map_blocks *map, int flags)
''',
'''int z_erofs_map_blocks(struct erofs_sb_info *sbi,
struct erofs_inode *vi, struct erofs_map_blocks *map)
''',
)
replace_once(
''' (void)sbi;
if (flags != 0)
return (EINVAL);
++gate_adapter_calls;
''',
''' (void)sbi;
++gate_adapter_calls;
''',
)
replace_once(
''' adapter_program += extract_function(data_source, "erofs_inline_tail_start")
adapter_program += extract_function(data_source, "erofs_map_blocks_chunk")
adapter_program += legacy_body
adapter_program += extract_function(data_source, "erofs_map_blocks")
''',
''' adapter_program += extract_function(data_source, "erofs_inline_tail_start")
adapter_program += extract_function(data_source, "erofs_map_blocks_chunk")
adapter_program += extract_function(data_source, "erofs_map_blocks_flatmode")
adapter_program += extract_function(data_source, "erofs_map_blocks")
''',
)
Path(sys.argv[2]).write_text(program + "\n", encoding="ascii")
PY
then
:
else
pre15_dut_fail 'B07c could not derive the current-consumer replay'
fi
if python3 "$artifacts/P15-006-B07c.py" "$PRE15_ROOT" "$input" \
worktree '' "$artifacts/candidate" "$oracle" \
>"$artifacts/candidate.stdout" 2>"$artifacts/candidate.stderr"; then
:
else
pre15_dut_fail 'B07c common consumer replay failed'
fi
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$oracle" \
"$artifacts/baseline" "$artifacts/candidate" "$artifacts/consumer-check.json" \
"$b07b" <<'PY'
from __future__ import annotations
from collections import Counter
import hashlib
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
oracle = json.loads(Path(sys.argv[3]).read_text(encoding="ascii"))
baseline = Path(sys.argv[4])
candidate = Path(sys.argv[5])
output = Path(sys.argv[6])
b07b = sys.argv[7]
def load(directory: Path, name: str) -> dict:
return json.loads((directory / name).read_text(encoding="ascii"))
def extract_function(source: str, name: str) -> str:
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if not match:
raise SystemExit(f"missing function: {name}")
start = source.rfind("\n\n", 0, match.start()) + 2
brace = source.find("{", match.end())
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
raise SystemExit(f"unterminated function: {name}")
def function_names(source: str) -> set[str]:
return set(re.findall(
r"^([A-Za-z_][A-Za-z0-9_]*)\s*\(", source, re.MULTILINE
))
def committed_source(path: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{b07b}:repo-pre-15/src/{path}"],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
raise SystemExit(f"cannot read B07b source {path}: {completed.stderr}")
return completed.stdout
def replace_once(source: str, old: str, new: str, label: str) -> str:
if source.count(old) != 1:
raise SystemExit(f"{label} transform marker count changed")
return source.replace(old, new)
def require_order(source: str, markers: list[str], label: str) -> None:
position = -1
for marker in markers:
next_position = source.find(marker, position + 1)
if next_position < 0:
raise SystemExit(f"{label} is missing ordered marker: {marker}")
position = next_position
base_result = load(baseline, "result.json")
base_tuples = load(baseline, "tuples.json")
base_devices = load(baseline, "devices.json")
result = load(candidate, "result.json")
tuples = load(candidate, "tuples.json")
devices = load(candidate, "devices.json")
for label, record in (("baseline", base_result), ("candidate", result)):
if record["status"] != "GO" or record["failures"]:
raise SystemExit(f"{label} P15-006 replay did not GO: {record!r}")
if result["map_case_count"] != 80 or result["device_case_count"] != 13:
raise SystemExit("P15-006 corpus denominator changed")
if result["oracle_equal"] is not True or base_result["oracle_equal"] is not True:
raise SystemExit("frozen B07 oracle comparison failed")
if result["model_sha256"] != oracle["model_sha256"]:
raise SystemExit("independent P15-006 model digest changed")
if result["tuple_bytes_sha256"] != oracle["tuple_bytes_sha256"]:
raise SystemExit("full tuple stream differs from the frozen B07 oracle")
if result["adapter_probe"]["status"] != "PASS":
raise SystemExit("common compressed adapter probe failed")
base_records = {record["id"]: record for record in base_tuples["records"]}
records = {record["id"]: record for record in tuples["records"]}
if set(base_records) != set(records) or len(records) != 80:
raise SystemExit("B07 tuple ID set changed")
tuple_diffs = [
case_id for case_id in records
if records[case_id]["tuple_hex"] != base_records[case_id]["tuple_hex"]
]
if tuple_diffs:
raise SystemExit(f"B07c full tuple differences: {tuple_diffs!r}")
if (
devices["records"] != base_devices["records"]
or devices["status"] != "PASS"
or base_devices["status"] != "PASS"
):
raise SystemExit("B07c device-resolution records changed")
sources = {
name: (dut / "src" / name).read_text(encoding="utf-8")
for name in ("internal.h", "data.c", "super.c", "zdata.c", "zmap.c")
}
old = {name: committed_source(name) for name in sources}
joined = "\n".join(sources.values())
if re.search(r"\berofs_map_blocks_legacy\b|\bz_erofs_map_blocks_iter\b", joined):
raise SystemExit("legacy map interface remains")
common = extract_function(sources["data.c"], "erofs_map_blocks")
read_data = extract_function(sources["data.c"], "erofs_read_data")
read_uio = extract_function(sources["data.c"], "erofs_read_uio")
zread = extract_function(sources["zdata.c"], "z_erofs_do_read")
metabox = extract_function(sources["super.c"], "erofs_init_metabox_inode")
backend = extract_function(sources["zmap.c"], "z_erofs_map_blocks")
if "z_erofs_map_blocks(sbi, vi, &next)" not in common:
raise SystemExit("common entry does not own compressed dispatch")
if backend.count("EROFS_GET_BLOCKS_FIEMAP") != 2:
raise SystemExit("compressed backend did not preserve FIEMAP semantics")
if sources["data.c"].count("erofs_map_blocks(sbi, vi, &map)") != 2:
raise SystemExit("data consumers did not both migrate")
if zread.count("erofs_map_blocks(sbi, vi, &map)") != 1:
raise SystemExit("z_erofs_do_read did not migrate")
if metabox.count("erofs_map_blocks(sbi, sbi->metabox_en, &map)") != 1:
raise SystemExit("metabox validation did not migrate")
require_order(
read_data,
[
"map = (struct erofs_map_blocks) { .m_la = loff + done };",
"error = erofs_map_blocks(sbi, vi, &map);",
"if (error != 0)",
"if (map.m_llen == 0)",
"want = MIN((size_t)map.m_llen, len - done);",
"(map.m_flags & EROFS_MAP_MAPPED) == 0",
"(map.m_flags & EROFS_MAP_META) != 0",
"erofs_read_metadata(sbi, vi->nid, map.m_pa",
"erofs_read_physical(sbi, map.m_deviceid, map.m_pa",
"erofs_put_metabuf(&buf);",
"erofs_brelse(blk);",
],
"erofs_read_data",
)
require_order(
read_uio,
[
"map = (struct erofs_map_blocks) { .m_la = uio->uio_offset };",
"error = erofs_map_blocks(sbi, vi, &map);",
"if (error != 0)",
"if (map.m_llen == 0)",
"want = MIN((size_t)map.m_llen, (size_t)uio->uio_resid);",
"(map.m_flags & EROFS_MAP_MAPPED) == 0",
"error = uiomove(zerobuf, zlen, uio);",
"(map.m_flags & EROFS_MAP_META) != 0",
"erofs_read_metadata(sbi, vi->nid, map.m_pa, want",
"erofs_read_physical(sbi, map.m_deviceid, map.m_pa",
"error = uiomove(buf.data, want, uio);",
"erofs_put_metabuf(&buf);",
"error = uiomove(blk, want, uio);",
"erofs_brelse(blk);",
],
"erofs_read_uio",
)
expected_internal = replace_once(
old["internal.h"],
"int z_erofs_map_blocks_iter(struct erofs_sb_info *sbi, struct erofs_inode *vi,\n"
" struct erofs_map_blocks *map, int flags);",
"int z_erofs_map_blocks(struct erofs_sb_info *sbi, struct erofs_inode *vi,\n"
" struct erofs_map_blocks *map);",
"internal.h",
)
if sources["internal.h"] != expected_internal:
raise SystemExit("B07c internal.h contains changes beyond backend closure")
expected_zdata = replace_once(
old["zdata.c"],
"\t\tbzero(&map, sizeof(map));\n"
"\t\tmap.m_la = loff + done;\n"
"\t\terror = z_erofs_map_blocks_iter(sbi, vi, &map,\n"
"\t\t EROFS_GET_BLOCKS_FIEMAP);",
"\t\tmap = (struct erofs_map_blocks) { .m_la = loff + done };\n"
"\t\terror = erofs_map_blocks(sbi, vi, &map);",
"zdata.c",
)
if sources["zdata.c"] != expected_zdata:
raise SystemExit("B07c zdata.c contains changes beyond consumer migration")
expected_super = replace_once(
old["super.c"],
"\t\t\tbzero(&map, sizeof(map));\n"
"\t\t\tmap.m_la = sbi->metabox_en->size - 1;\n"
"\t\t\terror = z_erofs_map_blocks_iter(sbi, sbi->metabox_en, &map,\n"
"\t\t\t EROFS_GET_BLOCKS_FIEMAP);",
"\t\t\tmap = (struct erofs_map_blocks) {\n"
"\t\t\t\t.m_la = sbi->metabox_en->size - 1,\n"
"\t\t\t};\n"
"\t\t\terror = erofs_map_blocks(sbi, sbi->metabox_en, &map);",
"super.c",
)
if sources["super.c"] != expected_super:
raise SystemExit("B07c super.c contains changes beyond metabox migration")
old_backend = extract_function(old["zmap.c"], "z_erofs_map_blocks_iter")
expected_backend = replace_once(
old_backend,
"z_erofs_map_blocks_iter(struct erofs_sb_info *sbi, struct erofs_inode *vi,\n"
" struct erofs_map_blocks *map, int flags)",
"z_erofs_map_blocks(struct erofs_sb_info *sbi, struct erofs_inode *vi,\n"
" struct erofs_map_blocks *map)",
"zmap signature",
)
expected_backend = expected_backend.replace(
"z_erofs_map_blocks_ext(sbi, vi, map, flags)",
"z_erofs_map_blocks_ext(sbi, vi, map,\n\t\t\t EROFS_GET_BLOCKS_FIEMAP)",
)
expected_backend = expected_backend.replace(
"z_erofs_map_blocks_fo(sbi, vi, map, flags)",
"z_erofs_map_blocks_fo(sbi, vi, map,\n\t\t\t EROFS_GET_BLOCKS_FIEMAP)",
)
if backend != expected_backend:
raise SystemExit("compressed backend changed beyond FIEMAP interface closure")
expected_zmap = old["zmap.c"].replace(old_backend, expected_backend)
if sources["zmap.c"] != expected_zmap:
raise SystemExit("B07c zmap.c contains changes outside the backend wrapper")
old_data_functions = function_names(old["data.c"])
data_functions = function_names(sources["data.c"])
if old_data_functions - data_functions != {"erofs_map_blocks_legacy"}:
raise SystemExit("B07c data.c removed functions beyond the legacy adapter")
if data_functions - old_data_functions:
raise SystemExit("B07c data.c added an unexpected function")
changed_data_functions = {
"erofs_map_blocks",
"erofs_read_data",
"erofs_read_uio",
}
for function in sorted(data_functions - changed_data_functions):
if extract_function(old["data.c"], function) != extract_function(
sources["data.c"], function
):
raise SystemExit(f"B07c changed protected data.c function: {function}")
cleanup = Counter(
(record["actual"]["acquire_count"], record["actual"]["release_count"])
for record in tuples["records"]
)
if any(acquire != release for acquire, release in cleanup):
raise SystemExit(f"metadata cleanup became unbalanced: {cleanup!r}")
if any(record["actual"]["errno"] < 0 for record in tuples["records"]):
raise SystemExit("negative errno observed")
tuple_bytes = b"".join(
bytes.fromhex(record["tuple_hex"]) for record in tuples["records"]
)
summary = {
"status": "PASS",
"final_id": "P15-006",
"map_cases": 80,
"producer_cases": 50,
"compressed_cases": 30,
"device_cases": 13,
"full_tuple_diff_count": len(tuple_diffs),
"tuple_bytes": len(tuple_bytes),
"tuple_bytes_sha256": hashlib.sha256(tuple_bytes).hexdigest(),
"model_sha256": result["model_sha256"],
"legacy_callsite_count": 0,
"data_consumers": 2,
"compressed_consumers": 2,
"backend_fiemap_dispatches": 2,
"cleanup_distribution": {
f"{acquire}:{release}": count
for (acquire, release), count in sorted(cleanup.items())
},
"qemu": "NOT_RUN",
"full_feature_suite": "NOT_RUN",
}
with output.open("x", encoding="ascii") as stream:
json.dump(summary, stream, ensure_ascii=True, indent=2, sort_keys=True)
stream.write("\n")
print("B07c PASS full=80 diff=0 device=13 legacy=0 consumers=4")
PY
then
:
else
pre15_dut_fail 'B07c consumer boundary check failed'
fi
sha256sum "$artifacts/baseline/result.json" \
"$artifacts/baseline/tuples.json" "$artifacts/baseline/devices.json" \
"$artifacts/candidate/result.json" "$artifacts/candidate/tuples.json" \
"$artifacts/candidate/devices.json" "$artifacts/consumer-check.json" \
>"$artifacts/SHA256SUMS"
printf 'B07c PASS common consumers; legacy map interfaces absent\n'
+907
View File
@@ -0,0 +1,907 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
gate=$PRE15_DUT/tests/pre15/gates/P15-006.sh
input=$PRE15_DUT/tests/pre15/gates/P15-006-input.json
oracle=$PRE15_DUT/tests/pre15/fixtures/B07-map-oracle.json
map_fixture=$PRE15_DUT/tests/pre15/fixtures/B08-map-runs.json
io_fixture=$PRE15_DUT/tests/pre15/fixtures/B08-io-caps.json
b07c_case=$PRE15_DUT/tests/pre15/cases/B07c-map-consumers.sh
artifacts=$PRE15_RUN_DIR/artifacts
baseline=6673f51152a5195a8a8903aa801f820abce7936e
b07c=55c609db13fc9b0f21a3c7c9ec5cb9574fd18276
for tool in cc git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B08 host tool: $tool"
done
pre15_record_fixture b08-gate "$gate"
pre15_record_fixture b08-input "$input"
pre15_record_fixture b08-b07-oracle "$oracle"
pre15_record_fixture b08-map-runs "$map_fixture"
pre15_record_fixture b08-io-caps "$io_fixture"
pre15_record_fixture b08-b07c-case "$b07c_case"
pre15_record_fixture b08-data "$PRE15_DUT/src/data.c"
mkdir -p "$artifacts"
pre15_target_reached
if "$gate" --base "$baseline" --oracle "$oracle" \
--output "$artifacts/baseline" >"$artifacts/baseline.stdout" \
2>"$artifacts/baseline.stderr"; then
:
else
pre15_dut_fail 'B08 frozen P15-006 baseline replay failed'
fi
if python3 - "$b07c_case" "$artifacts/derive-B07c.py" <<'PY'
from pathlib import Path
import sys
source = Path(sys.argv[1]).read_text(encoding="utf-8")
marker = 'if python3 - "$gate" "$artifacts/P15-006-B07c.py" <<\'PY\'\n'
start = source.index(marker) + len(marker)
end = source.index("\nPY\nthen", start)
Path(sys.argv[2]).write_text(source[start:end] + "\n", encoding="ascii")
PY
then
:
else
pre15_dut_fail 'B08 could not recover the proven B07c replay adapter'
fi
if python3 "$artifacts/derive-B07c.py" "$gate" \
"$artifacts/P15-006-B08.py" >"$artifacts/derive.stdout" \
2>"$artifacts/derive.stderr"; then
:
else
pre15_dut_fail 'B08 could not derive the current map replay'
fi
candidate_rc=0
python3 "$artifacts/P15-006-B08.py" "$PRE15_ROOT" "$input" \
worktree '' "$artifacts/candidate" "$oracle" \
>"$artifacts/candidate.stdout" 2>"$artifacts/candidate.stderr" || \
candidate_rc=$?
if test "$candidate_rc" -ne 1; then
pre15_dut_fail 'B08 old-run oracle did not reject exactly the intended change'
fi
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$oracle" "$map_fixture" \
"$io_fixture" "$artifacts/baseline" "$artifacts/candidate" \
"$artifacts/B08-result.json" "$b07c" "$artifacts" <<'PY'
from __future__ import annotations
from collections import Counter
import hashlib
import json
from pathlib import Path
import re
import struct
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
oracle_path = Path(sys.argv[3])
map_fixture_path = Path(sys.argv[4])
io_fixture_path = Path(sys.argv[5])
baseline = Path(sys.argv[6])
candidate = Path(sys.argv[7])
output = Path(sys.argv[8])
b07c = sys.argv[9]
artifacts = Path(sys.argv[10])
def load(path: Path) -> dict:
return json.loads(path.read_text(encoding="ascii"))
def sha256_bytes(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def extract_function(source: str, name: str) -> str:
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if not match:
raise SystemExit(f"missing function: {name}")
name_line = source.rfind("\n", 0, match.start()) + 1
start = source.rfind("\n", 0, name_line - 1) + 1
brace = source.find("{", match.end())
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
raise SystemExit(f"unterminated function: {name}")
def function_names(source: str) -> set[str]:
return set(re.findall(
r"^([A-Za-z_][A-Za-z0-9_]*)\s*\(", source, re.MULTILINE
))
def committed_data() -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{b07c}:repo-pre-15/src/data.c"],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
raise SystemExit(f"cannot read B07c data.c: {completed.stderr}")
return completed.stdout
def require_order(source: str, markers: list[str], label: str) -> None:
position = -1
for marker in markers:
next_position = source.find(marker, position + 1)
if next_position < 0:
raise SystemExit(f"{label} is missing ordered marker: {marker}")
position = next_position
def compile_and_run(name: str, program: str) -> list[str]:
source_path = artifacts / f"{name}.c"
binary_path = artifacts / name
source_path.write_text(program, encoding="ascii")
compiled = subprocess.run(
[
"cc", "-std=c11", "-O2", "-Wall", "-Wextra", "-Werror",
str(source_path), "-o", str(binary_path),
],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
(artifacts / f"{name}.compile.stdout").write_text(
compiled.stdout, encoding="utf-8"
)
(artifacts / f"{name}.compile.stderr").write_text(
compiled.stderr, encoding="utf-8"
)
if compiled.returncode != 0:
raise SystemExit(f"{name} compilation failed")
executed = subprocess.run(
[str(binary_path)],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
(artifacts / f"{name}.stdout").write_text(
executed.stdout, encoding="utf-8"
)
(artifacts / f"{name}.stderr").write_text(
executed.stderr, encoding="utf-8"
)
if executed.returncode != 0:
raise SystemExit(f"{name} execution failed")
return executed.stdout.splitlines()
oracle = load(oracle_path)
map_fixture = load(map_fixture_path)
io_fixture = load(io_fixture_path)
base_result = load(baseline / "result.json")
base_tuples = load(baseline / "tuples.json")
base_devices = load(baseline / "devices.json")
result = load(candidate / "result.json")
tuples = load(candidate / "tuples.json")
devices = load(candidate / "devices.json")
if sha256_bytes(oracle_path.read_bytes()) != map_fixture["b07_oracle_sha256"]:
raise SystemExit("B08 map fixture is not anchored to the frozen B07 oracle")
if map_fixture["schema"] != 1 or map_fixture["candidate"] != "P15-074":
raise SystemExit("invalid B08 map fixture identity")
if io_fixture["schema"] != 1 or io_fixture["candidate"] != "P15-074":
raise SystemExit("invalid B08 I/O fixture identity")
if (
base_result["status"] != "GO"
or base_result["oracle_equal"] is not True
or base_result["map_case_count"] != 80
or base_result["device_case_count"] != 13
):
raise SystemExit("frozen B07 control replay did not GO")
if result["status"] != "STOP" or result["map_case_count"] != 80:
raise SystemExit("B08 candidate did not expose the intended old-run delta")
if result["device_case_count"] != 13 or result["model_sha256"] != oracle["model_sha256"]:
raise SystemExit("B08 candidate changed the independent corpus denominator")
if result["adapter_probe"]["status"] != "PASS":
raise SystemExit("B08 common map adapter probe failed")
if devices["status"] != "PASS" or devices["records"] != base_devices["records"]:
raise SystemExit("B08 changed one of the 13 frozen device records")
tuple_fields = (
"m_la", "m_pa", "m_llen", "m_plen", "m_deviceid", "m_flags",
"m_algorithmformat", "errno", "acquire_count", "release_count",
)
tuple_layout = map_fixture["b07_tuple_layout"]
if tuple_layout != oracle["tuple_byte_layout"]:
raise SystemExit("B08 tuple byte layout drifted")
oracle_records = {record["id"]: record for record in oracle["records"]}
actual_records = {record["id"]: record for record in tuples["records"]}
base_records = {record["id"]: record for record in base_tuples["records"]}
transforms = {record["id"]: record for record in map_fixture["b07_transforms"]}
if len(oracle_records) != 80 or set(actual_records) != set(oracle_records):
raise SystemExit("B08 map tuple ID set changed")
if set(base_records) != set(oracle_records):
raise SystemExit("B08 baseline tuple ID set changed")
expected_bytes = bytearray()
actual_bytes = bytearray()
changed_ids = []
for record in oracle["records"]:
case_id = record["id"]
if base_records[case_id]["tuple_hex"] != record["tuple_hex"]:
raise SystemExit(f"frozen B07 baseline drifted: {case_id}")
expected = dict(zip(
tuple_fields,
struct.unpack(tuple_layout, bytes.fromhex(record["tuple_hex"])),
strict=True,
))
if case_id in transforms:
expected["m_llen"] = transforms[case_id]["m_llen"]
expected["m_plen"] = transforms[case_id]["m_plen"]
expected_tuple = struct.pack(
tuple_layout, *(expected[field] for field in tuple_fields)
)
actual_tuple = bytes.fromhex(actual_records[case_id]["tuple_hex"])
if actual_tuple != expected_tuple:
raise SystemExit(f"B08 transformed tuple mismatch: {case_id}")
if actual_tuple != bytes.fromhex(record["tuple_hex"]):
changed_ids.append(case_id)
expected_bytes.extend(expected_tuple)
actual_bytes.extend(actual_tuple)
if set(changed_ids) != set(transforms):
raise SystemExit(f"B08 changed unexpected B07 tuples: {changed_ids!r}")
transformed_hash = sha256_bytes(bytes(expected_bytes))
if bytes(actual_bytes) != bytes(expected_bytes):
raise SystemExit("B08 full transformed tuple stream mismatch")
if transformed_hash != map_fixture["expected_transformed_tuple_sha256"]:
raise SystemExit("B08 transformed tuple fixture digest mismatch")
allowed_failures = []
for failure in result["failures"]:
if failure.get("field") == "frozen-byte-oracle" and "id" not in failure:
continue
if failure.get("id") in transforms and failure.get("field") in {
"m_llen", "m_plen"
}:
continue
allowed_failures.append(failure)
if allowed_failures:
raise SystemExit(f"B08 old oracle reported unrelated failures: {allowed_failures!r}")
data_source = (dut / "src/data.c").read_text(encoding="utf-8")
old_data = committed_data()
expected_preamble = old_data[:old_data.index("static erofs_off_t\n")].replace(
"#include <sys/systm.h>\n",
"#include <sys/systm.h>\n#include <sys/_maxphys.h>\n",
)
if data_source[:data_source.index("static erofs_off_t\n")] != expected_preamble:
raise SystemExit("B08 data.c preamble changed beyond the MAXPHYS header")
old_functions = function_names(old_data)
data_functions = function_names(data_source)
if data_functions != old_functions:
raise SystemExit("B08 added or removed a data.c function")
changed_functions = {
"erofs_map_blocks_flatmode", "erofs_read_data", "erofs_read_uio"
}
for function in sorted(data_functions - changed_functions):
if extract_function(data_source, function) != extract_function(
old_data, function
):
raise SystemExit(f"B08 changed out-of-scope data.c function: {function}")
for function in changed_functions:
if extract_function(data_source, function) == extract_function(
old_data, function
):
raise SystemExit(f"B08 did not change required function: {function}")
flatmode = extract_function(data_source, "erofs_map_blocks_flatmode")
read_data = extract_function(data_source, "erofs_read_data")
read_uio = extract_function(data_source, "erofs_read_uio")
if flatmode.count("map->m_llen = remain;") != 1:
raise SystemExit("plain mapping does not expose the EOF run")
if flatmode.count("map->m_llen = MIN(remain, tail_start - loff);") != 1:
raise SystemExit("pre-inline mapping does not stop at the inline tail")
if "MAXPHYS" in flatmode:
raise SystemExit("B08 incorrectly capped the map contract")
if read_data.count("(uint64_t)MAXPHYS") != 1:
raise SystemExit("erofs_read_data lacks one MAXPHYS cap")
if read_uio.count("(uint64_t)MAXPHYS") != 1:
raise SystemExit("erofs_read_uio lacks one MAXPHYS cap")
require_order(
read_data,
[
"if (erofs_inode_is_data_compressed(vi->datalayout))",
"return (z_erofs_read_data(sbi, vi, loff, len, bufp));",
"error = erofs_map_blocks(sbi, vi, &map);",
"MIN(map.m_llen, (uint64_t)MAXPHYS)",
"erofs_read_physical(sbi, map.m_deviceid, map.m_pa",
"erofs_brelse(blk);",
],
"erofs_read_data",
)
require_order(
read_uio,
[
"if (erofs_inode_is_data_compressed(vi->datalayout))",
"return (z_erofs_read_uio(sbi, vi, uio));",
"error = erofs_map_blocks(sbi, vi, &map);",
"MIN(map.m_llen, (uint64_t)MAXPHYS)",
"erofs_read_physical(sbi, map.m_deviceid, map.m_pa",
"error = uiomove(blk, want, uio);",
"erofs_brelse(blk);",
],
"erofs_read_uio",
)
map_common = r'''
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#define EOPNOTSUPP 45
#define EOVERFLOW 84
#define EINTEGRITY 97
#define EROFS_NULL_ADDR UINT32_MAX
#define EROFS_INODE_FLAT_PLAIN 0
#define EROFS_INODE_COMPRESSED_FULL 1
#define EROFS_INODE_FLAT_INLINE 2
#define EROFS_INODE_COMPRESSED_COMPACT 3
#define EROFS_INODE_CHUNK_BASED 4
#define EROFS_MAP_MAPPED 0x0001
#define EROFS_MAP_META 0x0002
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define roundup2(x, y) (((x) + ((y) - 1)) & ~((y) - 1))
typedef uint64_t erofs_off_t;
typedef uint64_t erofs_blk_t;
struct erofs_sb_info {
uint64_t block_size;
unsigned int blkszbits;
};
struct erofs_inode {
uint64_t size;
erofs_off_t inode_off;
erofs_blk_t startblk;
uint8_t datalayout;
uint8_t inode_isize;
uint32_t xattr_isize;
};
struct erofs_map_blocks {
erofs_off_t m_pa, m_la;
uint64_t m_plen, m_llen;
unsigned short m_deviceid;
char m_algorithmformat;
unsigned int m_flags;
};
static bool
erofs_inode_is_data_compressed(unsigned int layout)
{
return (layout == EROFS_INODE_COMPRESSED_FULL ||
layout == EROFS_INODE_COMPRESSED_COMPACT);
}
static int
erofs_map_blocks_chunk(struct erofs_sb_info *sbi, struct erofs_inode *vi,
struct erofs_map_blocks *map)
{
(void)sbi;
(void)vi;
(void)map;
return (EOPNOTSUPP);
}
static int
z_erofs_map_blocks(struct erofs_sb_info *sbi, struct erofs_inode *vi,
struct erofs_map_blocks *map)
{
(void)sbi;
(void)vi;
(void)map;
return (EOPNOTSUPP);
}
'''
map_program = map_common
map_program += extract_function(data_source, "erofs_inline_tail_start") + "\n"
map_program += flatmode + "\n"
map_program += extract_function(data_source, "erofs_map_blocks") + "\n"
map_program += "\nint\nmain(void)\n{\n\tint error;\n"
for case in map_fixture["map_cases"]:
inode = case["inode"]
map_program += f'''
{{
struct erofs_sb_info sbi = {{ .block_size = 4096, .blkszbits = 12 }};
struct erofs_inode vi = {{
.size = UINT64_C({inode['size']}),
.inode_off = UINT64_C({inode.get('inode_off', 0)}),
.startblk = UINT64_C({inode['startblk']}),
.datalayout = {inode['layout']},
.inode_isize = {inode.get('inode_isize', 0)},
.xattr_isize = {inode.get('xattr_isize', 0)},
}};
struct erofs_map_blocks map = {{ .m_la = UINT64_C({case['request']}) }};
error = erofs_map_blocks(&sbi, &vi, &map);
printf("map\\t{case['id']}\\t%llu\\t%llu\\t%llu\\t%llu\\t%u\\t%u\\t%d\\t%d\\n",
(unsigned long long)map.m_la, (unsigned long long)map.m_pa,
(unsigned long long)map.m_llen, (unsigned long long)map.m_plen,
map.m_deviceid, map.m_flags, (int)map.m_algorithmformat, error);
}}
'''
map_program += "\treturn (0);\n}\n"
map_lines = compile_and_run("B08-map-extractor", map_program)
map_actual = {}
for line in map_lines:
fields = line.split("\t")
if len(fields) != 10 or fields[0] != "map":
raise SystemExit(f"invalid B08 map extractor line: {line!r}")
map_actual[fields[1]] = dict(zip(
(
"m_la", "m_pa", "m_llen", "m_plen", "m_deviceid",
"m_flags", "m_algorithmformat", "errno",
),
[int(value) for value in fields[2:]],
strict=True,
))
if set(map_actual) != {case["id"] for case in map_fixture["map_cases"]}:
raise SystemExit("B08 custom map case set changed")
positive_runs = 0
for case in map_fixture["map_cases"]:
actual = map_actual[case["id"]]
expected = {**case["expected"], "m_algorithmformat": 0}
if actual != expected:
raise SystemExit(
f"B08 custom map mismatch {case['id']}: {actual!r} != {expected!r}"
)
if (
actual["errno"] == 0
and case["request"] < case["inode"]["size"]
):
if actual["m_llen"] == 0:
raise SystemExit(f"B08 non-EOF zero run: {case['id']}")
positive_runs += 1
maxphys = io_fixture["harness_maxphys"]
consumer_common = f'''
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define EIO 5
#define ENOMEM 12
#define EINVAL 22
#define EOPNOTSUPP 45
#define EOVERFLOW 84
#define EINTEGRITY 97
#define PAGE_SIZE 4096
#define MAXPHYS {maxphys}
#define M_EROFS 0
#define M_WAITOK 0
#define EROFS_INODE_FLAT_PLAIN 0
#define EROFS_MAP_MAPPED 0x0001
#define EROFS_MAP_META 0x0002
#define EROFS_BUF_INITIALIZER {{ .data = NULL, .release = NULL }}
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define bzero(ptr, len) memset((ptr), 0, (len))
typedef uint64_t erofs_off_t;
typedef uint64_t erofs_nid_t;
struct erofs_sb_info {{ int unused; }};
struct erofs_inode {{
erofs_nid_t nid;
uint64_t size;
uint8_t datalayout;
}};
struct erofs_map_blocks {{
erofs_off_t m_pa, m_la;
uint64_t m_plen, m_llen;
unsigned short m_deviceid;
char m_algorithmformat;
unsigned int m_flags;
}};
struct erofs_buf {{
void *data;
void (*release)(void *);
}};
struct uio {{
int64_t uio_offset;
size_t uio_resid;
unsigned char *buffer;
size_t moved;
}};
static size_t gate_allocations;
static void *
gate_malloc(size_t size)
{{
void *buffer = malloc(size);
if (buffer != NULL)
++gate_allocations;
return (buffer);
}}
static void
gate_free(void *buffer)
{{
if (buffer != NULL) {{
--gate_allocations;
free(buffer);
}}
}}
#define malloc(size, type, flags) gate_malloc(size)
#define free(buffer, type) gate_free(buffer)
#define GATE_MAX_CALLS 32
#define GATE_PHYSICAL_BASE UINT64_C(1048576)
static bool gate_hole;
static unsigned int gate_map_calls;
static unsigned int gate_physical_calls;
static erofs_off_t gate_physical_offsets[GATE_MAX_CALLS];
static size_t gate_physical_lengths[GATE_MAX_CALLS];
static void
gate_reset(bool hole)
{{
gate_hole = hole;
gate_map_calls = 0;
gate_physical_calls = 0;
memset(gate_physical_offsets, 0, sizeof(gate_physical_offsets));
memset(gate_physical_lengths, 0, sizeof(gate_physical_lengths));
}}
static bool
erofs_inode_is_data_compressed(unsigned int layout)
{{
(void)layout;
return (false);
}}
static int
erofs_map_blocks(struct erofs_sb_info *sbi, struct erofs_inode *vi,
struct erofs_map_blocks *map)
{{
struct erofs_map_blocks next = {{ .m_la = map->m_la }};
(void)sbi;
++gate_map_calls;
if (next.m_la < vi->size) {{
next.m_pa = GATE_PHYSICAL_BASE + next.m_la;
next.m_llen = vi->size - next.m_la;
next.m_plen = next.m_llen;
if (!gate_hole)
next.m_flags = EROFS_MAP_MAPPED;
}}
*map = next;
return (0);
}}
static int
erofs_read_physical(struct erofs_sb_info *sbi, unsigned int device_id,
erofs_off_t off, size_t len, void **bufp)
{{
unsigned char *buffer;
size_t index;
(void)sbi;
(void)device_id;
if (len > MAXPHYS || gate_physical_calls == GATE_MAX_CALLS)
return (EINVAL);
gate_physical_offsets[gate_physical_calls] = off;
gate_physical_lengths[gate_physical_calls] = len;
++gate_physical_calls;
buffer = gate_malloc(len);
if (buffer == NULL)
return (ENOMEM);
for (index = 0; index < len; ++index)
buffer[index] = (unsigned char)(off - GATE_PHYSICAL_BASE + index);
*bufp = buffer;
return (0);
}}
static int
erofs_read_metadata(struct erofs_sb_info *sbi, erofs_nid_t nid,
erofs_off_t off, size_t len, struct erofs_buf *buf)
{{
(void)sbi;
(void)nid;
(void)off;
(void)len;
(void)buf;
return (EIO);
}}
static void
erofs_put_metabuf(struct erofs_buf *buf)
{{
(void)buf;
}}
static void
erofs_brelse(void *buffer)
{{
gate_free(buffer);
}}
static int
uiomove(const void *source, size_t len, struct uio *uio)
{{
if (len > uio->uio_resid)
return (EINVAL);
memcpy(uio->buffer + uio->moved, source, len);
uio->moved += len;
uio->uio_resid -= len;
uio->uio_offset += (int64_t)len;
return (0);
}}
static int
z_erofs_read_data(struct erofs_sb_info *sbi, struct erofs_inode *vi,
erofs_off_t loff, size_t len, void **bufp)
{{
(void)sbi;
(void)vi;
(void)loff;
(void)len;
(void)bufp;
return (EOPNOTSUPP);
}}
static int
z_erofs_read_uio(struct erofs_sb_info *sbi, struct erofs_inode *vi,
struct uio *uio)
{{
(void)sbi;
(void)vi;
(void)uio;
return (EOPNOTSUPP);
}}
static int
gate_check_data(const unsigned char *buffer, size_t length,
uint64_t offset, bool hole)
{{
size_t index;
for (index = 0; index < length; ++index) {{
unsigned char expected = hole ? 0 : (unsigned char)(offset + index);
if (buffer[index] != expected)
return (1);
}}
return (0);
}}
static int
gate_fail(const char *id, const char *reason)
{{
fprintf(stderr, "%s: %s\\n", id, reason);
return (1);
}}
'''
consumer_program = consumer_common
consumer_program += read_data + "\n"
consumer_program += read_uio + "\n"
consumer_program += "\nint\nmain(void)\n{\n"
for scenario in io_fixture["scenarios"]:
scenario_id = scenario["id"]
hole = scenario["mapping"] == "hole"
expected_offsets = scenario["expected_physical_offsets"]
expected_lengths = scenario["expected_physical_lengths"]
checks = []
for index, (offset, length) in enumerate(zip(
expected_offsets, expected_lengths, strict=True
)):
checks.append(
f"\t\tif (gate_physical_offsets[{index}] != UINT64_C({offset}) || "
f"gate_physical_lengths[{index}] != {length})\n"
f"\t\t\treturn (gate_fail(\"{scenario_id}\", \"physical call tuple\"));\n"
)
call_checks = "".join(checks)
if scenario["api"] == "read_data":
success_check = ""
if scenario["expected_errno"] == 0:
success_check = f'''
if (buffer == NULL || gate_check_data(buffer, {scenario['length']},
UINT64_C({scenario['offset']}), {'true' if hole else 'false'}) != 0)
return (gate_fail("{scenario_id}", "returned data"));
'''
else:
success_check = f'''
if (buffer != NULL)
return (gate_fail("{scenario_id}", "error buffer ownership"));
'''
consumer_program += f'''
{{
struct erofs_sb_info sbi = {{ 0 }};
struct erofs_inode vi = {{
.nid = 1, .size = UINT64_C({scenario['inode_size']}),
.datalayout = EROFS_INODE_FLAT_PLAIN,
}};
void *buffer = NULL;
int error;
gate_reset({'true' if hole else 'false'});
error = erofs_read_data(&sbi, &vi, UINT64_C({scenario['offset']}),
{scenario['length']}, &buffer);
if (error != {scenario['expected_errno']})
return (gate_fail("{scenario_id}", "errno"));
if (gate_map_calls != {scenario['expected_map_calls']} ||
gate_physical_calls != {len(expected_lengths)})
return (gate_fail("{scenario_id}", "call count"));
{call_checks}{success_check} gate_free(buffer);
if (gate_allocations != 0)
return (gate_fail("{scenario_id}", "allocation balance"));
printf("io\\t{scenario_id}\\tPASS\\t%u\\t%u\\n",
gate_map_calls, gate_physical_calls);
}}
'''
elif scenario["api"] == "read_uio":
consumer_program += f'''
{{
struct erofs_sb_info sbi = {{ 0 }};
struct erofs_inode vi = {{
.nid = 1, .size = UINT64_C({scenario['inode_size']}),
.datalayout = EROFS_INODE_FLAT_PLAIN,
}};
unsigned char *buffer = gate_malloc({scenario['length']});
struct uio uio = {{
.uio_offset = {scenario['offset']}, .uio_resid = {scenario['length']},
.buffer = buffer, .moved = 0,
}};
int error;
if (buffer == NULL)
return (gate_fail("{scenario_id}", "test allocation"));
memset(buffer, 0xa5, {scenario['length']});
gate_reset({'true' if hole else 'false'});
error = erofs_read_uio(&sbi, &vi, &uio);
if (error != {scenario['expected_errno']} ||
uio.uio_offset != {scenario['expected_final_offset']} ||
uio.uio_resid != {scenario['expected_resid']})
return (gate_fail("{scenario_id}", "uio result"));
if (gate_map_calls != {scenario['expected_map_calls']} ||
gate_physical_calls != {len(expected_lengths)})
return (gate_fail("{scenario_id}", "call count"));
{call_checks} if (uio.moved != {scenario['length'] - scenario['expected_resid']} ||
gate_check_data(buffer, uio.moved, UINT64_C({scenario['offset']}),
{'true' if hole else 'false'}) != 0)
return (gate_fail("{scenario_id}", "moved data"));
gate_free(buffer);
if (gate_allocations != 0)
return (gate_fail("{scenario_id}", "allocation balance"));
printf("io\\t{scenario_id}\\tPASS\\t%u\\t%u\\n",
gate_map_calls, gate_physical_calls);
}}
'''
else:
raise SystemExit(f"unknown B08 I/O API: {scenario['api']}")
consumer_program += "\treturn (0);\n}\n"
io_lines = compile_and_run("B08-io-extractor", consumer_program)
expected_io_ids = [scenario["id"] for scenario in io_fixture["scenarios"]]
actual_io_ids = []
for line in io_lines:
fields = line.split("\t")
if len(fields) != 5 or fields[0] != "io" or fields[2] != "PASS":
raise SystemExit(f"invalid B08 I/O extractor line: {line!r}")
actual_io_ids.append(fields[1])
if actual_io_ids != expected_io_ids:
raise SystemExit("B08 I/O scenario order or denominator changed")
cleanup = Counter(
(record["actual"]["acquire_count"], record["actual"]["release_count"])
for record in tuples["records"]
)
if any(acquire != release for acquire, release in cleanup):
raise SystemExit(f"B08 metadata cleanup became unbalanced: {cleanup!r}")
if any(record["actual"]["errno"] < 0 for record in tuples["records"]):
raise SystemExit("B08 observed a negative errno")
summary = {
"status": "PASS",
"final_id": "P15-074",
"b07_map_cases": 80,
"b07_unchanged_tuples": 80 - len(changed_ids),
"b07_intended_run_tuples": len(changed_ids),
"b07_unexpected_tuple_differences": 0,
"b07_device_cases": 13,
"b07_device_differences": 0,
"b07_model_sha256": result["model_sha256"],
"transformed_tuple_bytes": len(actual_bytes),
"transformed_tuple_sha256": sha256_bytes(bytes(actual_bytes)),
"custom_map_cases": len(map_fixture["map_cases"]),
"custom_positive_runs": positive_runs,
"io_scenarios": len(io_fixture["scenarios"]),
"harness_maxphys": maxphys,
"max_observed_physical_io": max(
length
for scenario in io_fixture["scenarios"]
for length in scenario["expected_physical_lengths"]
),
"cleanup_distribution": {
f"{acquire}:{release}": count
for (acquire, release), count in sorted(cleanup.items())
},
"compressed_scope": "UNCHANGED",
"vnode_backed_scope": "NOT_ADDED",
"qemu": "NOT_RUN",
"full_feature_suite": "NOT_RUN",
}
with output.open("x", encoding="ascii") as stream:
json.dump(summary, stream, ensure_ascii=True, indent=2, sort_keys=True)
stream.write("\n")
print(
"B08 PASS map=80 unchanged=73 intended=7 device=13 "
f"custom={len(map_fixture['map_cases'])} io={len(io_fixture['scenarios'])}"
)
PY
then
:
else
pre15_dut_fail 'B08 plain-run/MAXPHYS boundary check failed'
fi
sha256sum "$artifacts/baseline/result.json" \
"$artifacts/baseline/tuples.json" "$artifacts/baseline/devices.json" \
"$artifacts/candidate/result.json" "$artifacts/candidate/tuples.json" \
"$artifacts/candidate/devices.json" "$artifacts/B08-result.json" \
"$artifacts/B08-map-extractor" "$artifacts/B08-io-extractor" \
>"$artifacts/SHA256SUMS"
printf 'B08 PASS contiguous plain/pre-inline runs with MAXPHYS-capped I/O\n'
+375
View File
@@ -0,0 +1,375 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=8e85a8b64dd1a956376ff1cb31d2157bc73df8ad
artifacts=$PRE15_RUN_DIR/artifacts
inode=$PRE15_DUT/src/inode.c
vnops=$PRE15_DUT/src/erofs_vnops.c
super=$PRE15_DUT/src/super.c
internal=$PRE15_DUT/src/internal.h
namei=$PRE15_DUT/src/namei.c
for tool in git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B09 host tool: $tool"
done
pre15_record_fixture b09-inode "$inode"
pre15_record_fixture b09-vnops "$vnops"
pre15_record_fixture b09-super "$super"
pre15_record_fixture b09-internal "$internal"
pre15_record_fixture b09-namei "$namei"
pre15_record_fixture b09-case "$PRE15_DUT/tests/pre15/cases/B09-vnode.sh"
mkdir -p "$artifacts"
pre15_target_reached
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$artifacts" <<'PY'
from __future__ import annotations
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
baseline = sys.argv[3]
artifacts = Path(sys.argv[4])
src = dut / "src"
def committed(path: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/src/{path}"],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
raise SystemExit(f"cannot read B09 baseline {path}: {completed.stderr}")
return completed.stdout
def replace_once(source: str, old: str, new: str, label: str) -> str:
count = source.count(old)
if count != 1:
raise SystemExit(f"{label}: expected one transform source, found {count}")
return source.replace(old, new, 1)
def extract_function(source: str, name: str) -> str:
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if not match:
raise SystemExit(f"missing function: {name}")
name_line = source.rfind("\n", 0, match.start()) + 1
start = source.rfind("\n", 0, name_line - 1) + 1
brace = source.find("{", match.end())
if brace < 0:
raise SystemExit(f"missing function body: {name}")
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
raise SystemExit(f"unterminated function: {name}")
def initializer(source: str, declaration: str, prefix: str) -> list[tuple[str, str]]:
match = re.search(
re.escape(declaration) + r"\s*=\s*\{(.*?)\n\};", source, re.DOTALL
)
if not match:
raise SystemExit(f"initializer absent: {declaration}")
return re.findall(
rf"^\s*\.({prefix}_[A-Za-z0-9_]+)\s*=\s*([^,]+),",
match.group(1),
re.MULTILINE,
)
def require_order(source: str, markers: list[str], label: str) -> None:
position = -1
for marker in markers:
position = source.find(marker, position + 1)
if position < 0:
raise SystemExit(f"{label} is missing ordered marker: {marker}")
current = {
name: (src / name).read_text(encoding="utf-8")
for name in ("inode.c", "erofs_vnops.c", "super.c", "internal.h", "namei.c")
}
base = {name: committed(name) for name in current}
moved_marker = "static u_int\nerofs_vfs_hash(erofs_nid_t nid)"
moved_offset = base["inode.c"].find(moved_marker)
if moved_offset < 0:
raise SystemExit("B09 baseline vnode adapter block is absent")
moved_block = base["inode.c"][moved_offset:]
expected_inode = base["inode.c"][:moved_offset].rstrip("\n") + "\n"
if current["inode.c"] != expected_inode:
raise SystemExit("inode.c changed outside the exact vnode adapter movement")
if current["internal.h"] != base["internal.h"]:
raise SystemExit("internal.h changed despite no B09 interface delta")
if current["namei.c"] != base["namei.c"]:
raise SystemExit("namecache/lookup code changed outside the B09 write set")
expected_vnops = replace_once(
base["erofs_vnops.c"],
"#include <sys/extattr.h>\n",
"#include <sys/extattr.h>\n#include <sys/fnv_hash.h>\n",
"vnode hash include",
)
old_open = """\tif (vp->v_type == VREG) {
\t\tif (vnode_create_vobject(vp, vi->size, ap->a_td) != 0)
\t\t\treturn (ENOMEM);
\t}
"""
new_open = """\tif (vp->v_type == VREG)
\t\tvnode_create_vobject(vp, vi->size, ap->a_td);
"""
expected_vnops = replace_once(expected_vnops, old_open, new_open, "pager dead branch")
expected_vnops = replace_once(
expected_vnops,
"static int\nerofs_reclaim(struct vop_reclaim_args *ap)",
moved_block + "\nstatic int\nerofs_reclaim(struct vop_reclaim_args *ap)",
"vnode adapter placement",
)
if current["erofs_vnops.c"] != expected_vnops:
raise SystemExit("erofs_vnops.c differs from the two declared B09 transforms")
expected_super = replace_once(
base["super.c"], "static vfs_vget_t erofs_vgetf;\n", "", "vget declaration"
)
expected_super = replace_once(
expected_super,
"""static int
erofs_vgetf(struct mount *mp, ino_t ino, int flags, struct vnode **vpp)
{
\treturn (erofs_vget(mp, ino, flags, vpp));
}
""",
"",
"vget wrapper",
)
expected_super = replace_once(
expected_super,
"\t.vfs_vget = erofs_vgetf,",
"\t.vfs_vget = erofs_vget,",
"vfs_vget slot",
)
if current["super.c"] != expected_super:
raise SystemExit("super.c differs from the exact direct-vget transform")
moved_functions = (
"erofs_vfs_hash",
"erofs_vfs_hash_cmp",
"erofs_fill_vnode",
"erofs_vget",
)
for name in moved_functions:
if extract_function(base["inode.c"], name) != extract_function(
current["erofs_vnops.c"], name
):
raise SystemExit(f"moved function changed semantics: {name}")
definitions = {
path: len(re.findall(r"^" + re.escape(name) + r"\s*\(", text, re.MULTILINE))
for path, text in current.items()
}
if definitions["erofs_vnops.c"] != 1 or sum(definitions.values()) != 1:
raise SystemExit(f"B09 function ownership is not unique: {name} {definitions}")
base_vnode_slots = initializer(
base["erofs_vnops.c"], "struct vop_vector erofs_vnodeops", "vop"
)
base_fifo_slots = initializer(
base["erofs_vnops.c"], "struct vop_vector erofs_fifoops", "vop"
)
vnode_slots = initializer(
current["erofs_vnops.c"], "struct vop_vector erofs_vnodeops", "vop"
)
fifo_slots = initializer(
current["erofs_vnops.c"], "struct vop_vector erofs_fifoops", "vop"
)
base_vfs_slots = initializer(base["super.c"], "static struct vfsops erofs_vfsops", "vfs")
vfs_slots = initializer(current["super.c"], "static struct vfsops erofs_vfsops", "vfs")
expected_vfs_slots = [
(slot, "erofs_vget" if slot == "vfs_vget" else target)
for slot, target in base_vfs_slots
]
if vnode_slots != base_vnode_slots or fifo_slots != base_fifo_slots:
raise SystemExit("FreeBSD KOBJ VOP slot order or targets changed")
if vfs_slots != expected_vfs_slots:
raise SystemExit("VFS slots changed beyond direct erofs_vget registration")
if ("vop_lookup", "vfs_cache_lookup") not in vnode_slots or (
"vop_cachedlookup", "erofs_lookup"
) not in vnode_slots:
raise SystemExit("FreeBSD namecache slots were not preserved")
if "erofs_vgetf" in current["super.c"]:
raise SystemExit("trivial erofs_vgetf wrapper remains")
vget = extract_function(current["erofs_vnops.c"], "erofs_vget")
require_order(
vget,
[
"td = curthread;",
"nid = (uint64_t)ino;",
"shared = (flags & LK_TYPE_MASK) == LK_SHARED;",
"hash = erofs_vfs_hash(nid);",
"error = vfs_hash_get(",
"if (error != 0 || *vpp != NULL)",
"sbi = MTOE(mp);",
"vi = malloc(sizeof(*vi), M_EROFS, M_WAITOK | M_ZERO);",
"error = getnewvnode(\"erofs\", mp, &erofs_vnodeops, &vp);",
"vp->v_data = vi;",
"vi->nid = nid;",
"lockmgr(vp->v_vnlock, LK_EXCLUSIVE, NULL);",
"error = insmntque(vp, mp);",
"error = vfs_hash_insert(",
"if (error != 0 || *vpp != NULL)",
"error = erofs_read_inode(sbi, nid, vi);",
"vgone(vp);",
"vput(vp);",
"erofs_fill_vnode(sbi, vp, vi);",
"vn_set_state(vp, VSTATE_CONSTRUCTED);",
"if (shared)",
"VOP_LOCK(vp, LK_DOWNGRADE);",
"*vpp = vp;",
],
"erofs_vget lifecycle",
)
if vget.count("erofs_vfs_hash_cmp") != 2:
raise SystemExit("vget no longer uses one comparator at both hash callsites")
if vget.count("free(vi, M_EROFS);") != 2 or vget.count("*vpp = NULL;") != 3:
raise SystemExit("vget allocation/insmntque/read failure cleanup changed")
if vget.count("vgone(vp);") != 1 or vget.count("vput(vp);") != 1:
raise SystemExit("vget failed-inode vnode cleanup changed")
if re.search(r"return\s*\(\s*-", vget):
raise SystemExit("negative errno entered the FreeBSD vget path")
reclaim = extract_function(current["erofs_vnops.c"], "erofs_reclaim")
if reclaim != extract_function(base["erofs_vnops.c"], "erofs_reclaim"):
raise SystemExit("reclaim/hash removal semantics changed")
require_order(
reclaim,
["vi = VTOE(vp);", "vfs_hash_remove(vp);", "free(vi, M_EROFS);", "vp->v_data = NULL;"],
"erofs_reclaim lifecycle",
)
open_body = extract_function(current["erofs_vnops.c"], "erofs_open")
if open_body.count("vnode_create_vobject(vp, vi->size, ap->a_td);") != 1:
raise SystemExit("regular-vnode pager setup is not a single direct call")
if "ENOMEM" in open_body or "vnode_create_vobject" not in open_body:
raise SystemExit("dead pager errno folding remains")
slot_report = {
"status": "PASS",
"vnode": [{"slot": slot, "target": target} for slot, target in vnode_slots],
"fifo": [{"slot": slot, "target": target} for slot, target in fifo_slots],
"vfs": [{"slot": slot, "target": target} for slot, target in vfs_slots],
}
(artifacts / "B09-vop-slots.json").write_text(
json.dumps(slot_report, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
encoding="ascii",
)
callgraph = [
"VFS root -> erofs_vget",
"namecache cachedlookup -> erofs_lookup -> erofs_vget",
"VFS vget slot -> erofs_vget",
"erofs_vget -> vfs_hash_get[erofs_vfs_hash_cmp]",
"erofs_vget -> getnewvnode -> LK_EXCLUSIVE -> insmntque",
"erofs_vget -> vfs_hash_insert[erofs_vfs_hash_cmp]",
"erofs_vget -> erofs_read_inode -> erofs_fill_vnode",
"erofs_vget -> VSTATE_CONSTRUCTED -> optional LK_DOWNGRADE",
"erofs_reclaim -> vfs_hash_remove -> free inode",
]
(artifacts / "B09-vget-callgraph.txt").write_text(
"\n".join(callgraph) + "\n", encoding="ascii"
)
result = {
"status": "PASS",
"final_ids": ["P15-008", "P15-050", "P15-088"],
"baseline": baseline,
"moved_functions": list(moved_functions),
"moved_functions_byte_identical": True,
"vnode_slots": len(vnode_slots),
"fifo_slots": len(fifo_slots),
"vfs_slots": len(vfs_slots),
"hash_comparator_callsites": 2,
"exclusive_construct_lock": True,
"shared_result_downgrade": True,
"insmntque_cleanup_preserved": True,
"reclaim_hash_removal_preserved": True,
"namecache_slots_preserved": True,
"positive_errno_preserved": True,
"b11_b15_scope": "NOT_INCLUDED",
"qemu": "NOT_RUN",
"full_feature_suite": "NOT_RUN",
}
(artifacts / "B09-result.json").write_text(
json.dumps(result, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
encoding="ascii",
)
print(
f"B09 PASS moved={len(moved_functions)} "
f"slots={len(vnode_slots)}+{len(fifo_slots)}/{len(vfs_slots)} hash=2"
)
PY
then
:
else
pre15_dut_fail 'B09 vnode ownership, VOP slots, or vget graph check failed'
fi
sha256sum "$artifacts/B09-vop-slots.json" \
"$artifacts/B09-vget-callgraph.txt" "$artifacts/B09-result.json" \
> "$artifacts/SHA256SUMS"
printf 'B09 PASS vnode adapter ownership and FreeBSD lifecycle preserved\n'
+36
View File
@@ -0,0 +1,36 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=22965aea267c3fcaafb67c14f9a246a7bc6195b1
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
spec=$fixture_dir/B10-directory-spec.json
oracle=$fixture_dir/B10-directory-oracle.py
artifacts=$PRE15_RUN_DIR/artifacts
report=$artifacts/B10-directory-results.json
for tool in git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B10 host tool: $tool"
done
pre15_record_fixture b10-spec "$spec"
pre15_record_fixture b10-oracle "$oracle"
pre15_record_fixture b10-case "$PRE15_DUT/tests/pre15/cases/B10-directory.sh"
pre15_record_fixture b10-dir "$PRE15_DUT/src/dir.c"
pre15_record_fixture b10-namei "$PRE15_DUT/src/namei.c"
mkdir -p "$artifacts"
pre15_target_reached
if python3 -B "$oracle" --root "$PRE15_ROOT" --dut "$PRE15_DUT" \
--baseline "$baseline" --spec "$spec" --report "$report"
then
pre15_record_fixture b10-report "$report"
else
pre15_dut_fail 'B10 directory trust-boundary oracle failed'
fi
+46
View File
@@ -0,0 +1,46 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=e2e3fb86b6fffcb01d6fd29c17dd95628ad070de
gate=$PRE15_DUT/tests/pre15/gates/P15-081.sh
input=$PRE15_DUT/tests/pre15/gates/P15-081-input.json
spec=$PRE15_DUT/tests/pre15/fixtures/B11-dtype-spec.json
oracle=$PRE15_DUT/tests/pre15/fixtures/B11-dtype-oracle.py
artifacts=$PRE15_RUN_DIR/artifacts
gate_output=$artifacts/gate
for tool in cc dump.erofs fsck.erofs git mkfs.erofs python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B11 host tool: $tool"
done
pre15_record_fixture b11-gate "$gate"
pre15_record_fixture b11-gate-input "$input"
pre15_record_fixture b11-spec "$spec"
pre15_record_fixture b11-oracle "$oracle"
for source in inode.c internal.h namei.c dir.c erofs_vnops.c; do
pre15_record_fixture "b11-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
if "$gate" --base "$baseline" --output "$gate_output" \
>"$artifacts/gate.stdout" 2>"$artifacts/gate.stderr"; then
:
else
pre15_runner_fail 'P15-081 frozen gate replay failed'
fi
pre15_target_reached
if python3 -B "$oracle" --root "$PRE15_ROOT" --dut "$PRE15_DUT" \
--spec "$spec" --gate-output "$gate_output" --artifacts "$artifacts"
then
pre15_record_fixture b11-report "$artifacts/B11-dtype-report.json"
else
pre15_dut_fail 'B11 dtype source or fixture oracle failed'
fi
+217
View File
@@ -0,0 +1,217 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
policy=$fixture_dir/B12-readahead-policy.json
kld_builder=$fixture_dir/B12-build-kld.sh
artifacts=$PRE15_RUN_DIR/artifacts
for tool in awk clang file git nm python3 sha256sum timeout; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B12 host tool: $tool"
done
pre15_record_fixture b12-policy "$policy"
pre15_record_fixture b12-kld-builder "$kld_builder"
pre15_record_fixture b12-case "$PRE15_DUT/tests/pre15/cases/B12-readahead.sh"
for source in data.c dir.c internal.h; do
pre15_record_fixture "b12-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
pre15_target_reached
if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$policy" \
"$artifacts/B12-policy-report.json" <<'PY'
from __future__ import annotations
import csv
import hashlib
import json
from pathlib import Path
import re
import statistics
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
policy_path = Path(sys.argv[3])
report_path = Path(sys.argv[4])
policy = json.loads(policy_path.read_text(encoding="ascii"))
if policy.get("schema") != 1 or policy.get("candidate") != "P15-087":
raise SystemExit("invalid B12 policy identity")
def digest(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
source_hashes = {
relative: digest(dut / relative)
for relative in policy["source_sha256"]
}
if source_hashes != policy["source_sha256"]:
raise SystemExit("B12 production source differs from the accepted source set")
changed = subprocess.run(
[
"git", "-C", str(root), "diff", "--name-only",
policy["baseline"], "--", "repo-pre-15/src",
],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()
expected_changed = [
"repo-pre-15/src/data.c",
"repo-pre-15/src/dir.c",
"repo-pre-15/src/internal.h",
]
if sorted(changed) != expected_changed:
raise SystemExit(f"B12 production write set mismatch: {changed}")
data = (dut / "src/data.c").read_text(encoding="utf-8")
directory = (dut / "src/dir.c").read_text(encoding="utf-8")
internal = (dut / "src/internal.h").read_text(encoding="utf-8")
if "cluster_read(" in data + directory + internal:
raise SystemExit("B12 must not introduce cluster_read")
if data.count("breadn(") != 1:
raise SystemExit("B12 must contain one backing-vnode breadn call")
if not re.search(r"breadn\(dif->devvp,", data):
raise SystemExit("B12 breadn is not issued on erofs_device_info.devvp")
required_data = (
"#define EROFS_DIR_READAHEAD_BYTES\t(1024 * 1024)",
"#define EROFS_DIR_READAHEAD_SLOTS\t(EROFS_DIR_READAHEAD_BYTES / PAGE_SIZE)",
"future.m_dif != current.m_dif",
"future.m_pa != current.m_pa + step",
"vi->datalayout == EROFS_INODE_FLAT_PLAIN",
"return (erofs_read_data_impl(sbi, vi, loff, len, 0, bufp));",
)
for marker in required_data:
if marker not in data:
raise SystemExit(f"missing B12 data policy marker: {marker}")
if directory.count("erofs_read_data_readahead(") != 1:
raise SystemExit("B12 directory hint is not limited to readdir")
if "sequential = logical_off == 0;" not in directory:
raise SystemExit("B12 random seek no-op predicate is absent")
if internal.count("erofs_read_data_readahead(") != 1:
raise SystemExit("B12 internal prototype is absent or duplicated")
evidence = root / policy["gate_evidence"]
result_path = evidence / "result.json"
rows_path = evidence / "runs.tsv"
if digest(result_path) != policy["gate_result_sha256"]:
raise SystemExit("B12 gate result hash mismatch")
if digest(rows_path) != policy["gate_rows_sha256"]:
raise SystemExit("B12 gate rows hash mismatch")
result = json.loads(result_path.read_text(encoding="ascii"))
if result.get("status") != "GO" or result.get("b12") != "AUTHORIZED":
raise SystemExit("P15-087 did not authorize B12")
if result.get("qemu") != "PASS" or not all(result.get("checks", {}).values()):
raise SystemExit("P15-087 runtime or semantic checks are incomplete")
thresholds = result.get("thresholds", {})
if thresholds != {
"maximum_extra_provider_reads_percent": policy["maximum_extra_provider_reads_percent"],
"minimum_cold_median_improvement_percent": policy["minimum_cold_median_improvement_percent"],
}:
raise SystemExit("P15-087 thresholds differ from the frozen B12 policy")
rows = []
with rows_path.open(encoding="ascii", newline="") as source:
for raw in csv.reader(source, delimiter="\t"):
if len(raw) != 10:
raise SystemExit(f"invalid B12 gate row: {raw}")
rows.append({
"variant": raw[0],
"run": raw[1],
"mode": raw[3],
"elapsed_ns": int(raw[4]),
"provider_reads": int(raw[5]),
"entry_count": int(raw[7]),
"hash": raw[8],
"final_cookie": int(raw[9]),
})
sequential = [row for row in rows if row["mode"] == "sequential"]
random_rows = [row for row in rows if row["mode"] == "random"]
baseline = [row for row in sequential if row["variant"] == "baseline"]
candidate = [row for row in sequential if row["variant"] == "candidate"]
if len(baseline) != 5 or len(candidate) != 5 or len(random_rows) != 2:
raise SystemExit("B12 gate does not contain the complete 5+5 and 1+1 sample set")
baseline_ns = statistics.median(row["elapsed_ns"] for row in baseline)
candidate_ns = statistics.median(row["elapsed_ns"] for row in candidate)
improvement = (baseline_ns - candidate_ns) * 100.0 / baseline_ns
baseline_reads = statistics.median(row["provider_reads"] for row in baseline)
candidate_reads = statistics.median(row["provider_reads"] for row in candidate)
extra_reads = (candidate_reads - baseline_reads) * 100.0 / baseline_reads
if improvement < policy["minimum_cold_median_improvement_percent"]:
raise SystemExit("B12 recomputed cold median improvement misses the gate")
if extra_reads > policy["maximum_extra_provider_reads_percent"]:
raise SystemExit("B12 recomputed provider-read delta misses the gate")
if any(
(row["entry_count"], row["hash"], row["final_cookie"])
!= (18002, "427bb414efa99dc0", 3880368)
for row in sequential
):
raise SystemExit("B12 sequential correctness rows differ from the oracle")
if {
(row["provider_reads"], row["entry_count"], row["hash"], row["final_cookie"])
for row in random_rows
} != {(19, 292, "9805eadece9f500e", 1998932)}:
raise SystemExit("B12 random path is not an exact no-op pair")
report = {
"baseline_samples": len(baseline),
"candidate_samples": len(candidate),
"baseline_median_elapsed_ns": baseline_ns,
"candidate_median_elapsed_ns": candidate_ns,
"cold_median_improvement_percent": improvement,
"extra_provider_reads_percent": extra_reads,
"gate": "GO",
"qemu_runtime": "PASS",
"random_samples": len(random_rows),
"source_sha256": source_hashes,
"status": "PASS",
"window_bytes": policy["maximum_readahead_bytes"],
}
report_path.write_text(
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii"
)
PY
then
:
else
pre15_dut_fail 'B12 source, write-set, or frozen G11 policy replay failed'
fi
pre15_record_fixture b12-policy-report "$artifacts/B12-policy-report.json"
module=$PRE15_CASE_TMP/B12-erofs-zstdio0.ko
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
"$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" \
>"$artifacts/B12-kld-build.stdout" 2>"$artifacts/B12-kld-build.stderr"; then
pre15_dut_fail 'B12 exact-ABI zstdio0 KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/B12-kld-file.txt"
sha256sum "$module" >"$artifacts/B12-kld-sha256.txt"
nm -g "$module" | LC_ALL=C sort >"$artifacts/B12-kld-nm-global.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/B12-kld-nm-u.txt"
if ! awk '$NF == "erofs_read_data_readahead" { found = 1 } END { exit !found }' \
"$artifacts/B12-kld-nm-global.txt"; then
pre15_dut_fail 'B12 exact-ABI KLD lacks the directory readahead symbol'
fi
if grep -q ' ZSTD_' "$artifacts/B12-kld-nm-u.txt"; then
pre15_dut_fail 'B12 zstdio0 KLD contains an unexpected Zstd symbol'
fi
printf '%s\n' \
'B12-policy PASS: exact source/write set and frozen 5+5 G11 statistics' \
'K zstdio0 PASS: FreeBSD 15 exact-ABI cross-KLD and symbol contract' \
'TC183 correctness/performance PASS from immutable P15-087 QEMU evidence'
+604
View File
@@ -0,0 +1,604 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=08a567554881f19bcec933a92a46d18cfb73d22e
artifacts=$PRE15_RUN_DIR/artifacts
compact_fixture=$PRE15_DUT/tests/pre15/fixtures/B13-compact-time.json
extended_fixture=$PRE15_DUT/tests/pre15/fixtures/B13-extended-time.json
inode=$PRE15_DUT/src/inode.c
internal=$PRE15_DUT/src/internal.h
super=$PRE15_DUT/src/super.c
ondisk=$PRE15_DUT/src/erofs_fs.h
vnops=$PRE15_DUT/src/erofs_vnops.c
for tool in cc git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B13 host tool: $tool"
done
pre15_record_fixture b13-compact "$compact_fixture"
pre15_record_fixture b13-extended "$extended_fixture"
pre15_record_fixture b13-inode "$inode"
pre15_record_fixture b13-internal "$internal"
pre15_record_fixture b13-super "$super"
pre15_record_fixture b13-ondisk "$ondisk"
pre15_record_fixture b13-vnops "$vnops"
pre15_record_fixture b13-case "$PRE15_DUT/tests/pre15/cases/B13-time.sh"
mkdir -p "$artifacts"
pre15_target_reached
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$compact_fixture" \
"$extended_fixture" "$artifacts" <<'PY'
from __future__ import annotations
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
baseline = sys.argv[3]
compact_path = Path(sys.argv[4])
extended_path = Path(sys.argv[5])
artifacts = Path(sys.argv[6])
src = dut / "src"
eintegrity = 97
int64_min = -(1 << 63)
int64_max = (1 << 63) - 1
int32_min = -(1 << 31)
int32_max = (1 << 31) - 1
def committed(path: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/src/{path}"],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
raise SystemExit(f"cannot read B13 baseline {path}: {completed.stderr}")
return completed.stdout
def replace_once(source: str, old: str, new: str, label: str) -> str:
count = source.count(old)
if count != 1:
raise SystemExit(f"{label}: expected one transform source, found {count}")
return source.replace(old, new, 1)
def extract_function(source: str, name: str) -> str:
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if not match:
raise SystemExit(f"missing function: {name}")
name_line = source.rfind("\n", 0, match.start()) + 1
start = source.rfind("\n", 0, name_line - 1) + 1
brace = source.find("{", match.end())
if brace < 0:
raise SystemExit(f"missing function body: {name}")
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
raise SystemExit(f"unterminated function: {name}")
def load_fixture(path: Path, expected_encoding: set[str]) -> dict:
fixture = json.loads(path.read_text(encoding="ascii"))
if fixture.get("schema") != 1 or fixture.get("candidate") != "P15-018":
raise SystemExit(f"invalid B13 fixture identity: {path.name}")
if set(fixture.get("encoding", {})) != expected_encoding:
raise SystemExit(f"invalid B13 fixture encoding keys: {path.name}")
if not isinstance(fixture.get("cases"), list) or not fixture["cases"]:
raise SystemExit(f"empty B13 fixture: {path.name}")
return fixture
def decode_le(raw: str, width: int, *, signed: bool) -> int:
if not re.fullmatch(r"[0-9a-f]+", raw) or len(raw) != width * 2:
raise SystemExit(f"invalid {width}-byte lowercase hex value: {raw!r}")
return int.from_bytes(bytes.fromhex(raw), byteorder="little", signed=signed)
def status_for_range(value: int, minimum: int, maximum: int) -> str:
return "PASS" if minimum <= value <= maximum else "EINTEGRITY"
current = {
name: (src / name).read_text(encoding="utf-8")
for name in ("internal.h", "inode.c", "super.c", "erofs_fs.h", "erofs_vnops.c")
}
base = {name: committed(name) for name in current}
expected_internal = replace_once(
base["internal.h"], "\tuint64_t epoch;", "\tint64_t epoch;", "signed epoch field"
)
expected_internal = replace_once(
expected_internal, "\tuint64_t mtime;", "\ttime_t mtime;", "time_t inode field"
)
if current["internal.h"] != expected_internal:
raise SystemExit("internal.h differs from the two declared B13 type changes")
old_set_timestamp = """static int
erofs_set_timestamp(struct erofs_inode *vi, uint64_t seconds,
uint32_t nanoseconds)
{
\tif (nanoseconds >= 1000000000 || seconds > (uint64_t)INT64_MAX)
\t\treturn (EINTEGRITY);
\tvi->mtime = seconds;
\tvi->mtime_nsec = nanoseconds;
\treturn (0);
}
"""
new_set_timestamp = """static int
erofs_set_timestamp(struct erofs_inode *vi, int64_t seconds,
uint32_t nanoseconds)
{
\ttime_t mtime;
\tif (nanoseconds >= 1000000000 ||
\t __builtin_add_overflow(seconds, 0, &mtime))
\t\treturn (EINTEGRITY);
\tvi->mtime = mtime;
\tvi->mtime_nsec = nanoseconds;
\treturn (0);
}
"""
expected_inode = replace_once(
base["inode.c"], old_set_timestamp, new_set_timestamp, "timestamp conversion"
)
expected_inode = replace_once(
expected_inode,
"\tuint64_t addrmask, mtime;",
"\tuint64_t addrmask;\n\tint64_t mtime;",
"signed decoded mtime",
)
expected_inode = replace_once(
expected_inode,
"\t\t (uint64_t)le32toh(dic->i_mtime), &mtime)",
"\t\t (int64_t)le32toh(dic->i_mtime), &mtime)",
"compact signed checked add",
)
expected_inode = replace_once(
expected_inode,
"\t\terror = erofs_set_timestamp(vi, le64toh(die->i_mtime),\n"
"\t\t le32toh(die->i_mtime_nsec));",
"\t\terror = erofs_set_timestamp(vi,\n"
"\t\t (int64_t)le64toh(die->i_mtime),\n"
"\t\t le32toh(die->i_mtime_nsec));",
"extended signed decode",
)
if current["inode.c"] != expected_inode:
raise SystemExit("inode.c differs from the four declared B13 transforms")
expected_super = replace_once(
base["super.c"],
"\tsbi->epoch = le64toh(dsb->epoch);",
"\tsbi->epoch = (int64_t)le64toh(dsb->epoch);",
"super signed epoch decode",
)
if current["super.c"] != expected_super:
raise SystemExit("super.c differs from the exact signed epoch transform")
if current["erofs_fs.h"] != base["erofs_fs.h"]:
raise SystemExit("B13 changed ondisk endian types, widths, or layout")
if current["erofs_vnops.c"] != base["erofs_vnops.c"]:
raise SystemExit("B13 changed FreeBSD VOP/timespec publication code")
ondisk_required = (
"\t__le64 epoch;",
"\t__le32 i_mtime;",
"\t__le64 i_mtime;",
"\t__le32 i_mtime_nsec;",
)
for declaration in ondisk_required:
if current["erofs_fs.h"].count(declaration) != 1:
raise SystemExit(f"ondisk timestamp declaration changed: {declaration}")
timestamp_helper = extract_function(current["inode.c"], "erofs_set_timestamp")
read_inode = extract_function(current["inode.c"], "erofs_read_inode")
if timestamp_helper.count("__builtin_add_overflow(seconds, 0, &mtime)") != 1:
raise SystemExit("time_t checked conversion is absent or duplicated")
compact_pattern = re.compile(
r"__builtin_add_overflow\(sbi->epoch,\s*"
r"\(int64_t\)le32toh\(dic->i_mtime\), &mtime\)"
)
if len(compact_pattern.findall(read_inode)) != 1:
raise SystemExit("compact signed checked addition is absent or duplicated")
if read_inode.count("(int64_t)le64toh(die->i_mtime)") != 1:
raise SystemExit("extended signed seconds decode is absent or duplicated")
if "(uint64_t)le32toh(dic->i_mtime)" in read_inode:
raise SystemExit("unsigned compact timestamp arithmetic remains")
changed_sources = current["internal.h"] + current["inode.c"] + current["super.c"]
if "ckd_add" in changed_sources or "stdckdint" in changed_sources:
raise SystemExit("unsupported checked-arithmetic API entered B13")
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", changed_sources):
raise SystemExit("negative errno entered the FreeBSD timestamp path")
overflow_lines = []
for path in ("inode.c", "super.c"):
for line_number, line in enumerate(current[path].splitlines(), start=1):
if "__builtin_" in line and "overflow" in line:
overflow_lines.append(f"{path}:{line_number}:{line.strip()}")
(artifacts / "B13-overflow-scan.txt").write_text(
"\n".join(overflow_lines) + "\n", encoding="ascii"
)
compact = load_fixture(
compact_path, {"epoch", "fixed_nsec", "mtime_delta"}
)
extended = load_fixture(extended_path, {"mtime", "mtime_nsec"})
seen_ids: set[str] = set()
decoded_compact = []
decoded_extended = []
for case in compact["cases"]:
case_id = case.get("id")
if not isinstance(case_id, str) or case_id in seen_ids:
raise SystemExit(f"invalid or duplicate B13 case id: {case_id!r}")
seen_ids.add(case_id)
epoch = decode_le(case["epoch_le"], 8, signed=True)
delta = decode_le(case["mtime_delta_le"], 4, signed=False)
nanoseconds = decode_le(case["fixed_nsec_le"], 4, signed=False)
mathematical = epoch + delta
add_status = status_for_range(mathematical, int64_min, int64_max)
if add_status != case["expected_add"]:
raise SystemExit(f"compact add oracle mismatch: {case_id}")
if add_status == "PASS":
if mathematical != case["expected_seconds"]:
raise SystemExit(f"compact seconds oracle mismatch: {case_id}")
if status_for_range(mathematical, int64_min, int64_max) != case["expected_time64"]:
raise SystemExit(f"compact time64 oracle mismatch: {case_id}")
if status_for_range(mathematical, int32_min, int32_max) != case["expected_time32"]:
raise SystemExit(f"compact time32 oracle mismatch: {case_id}")
nsec_status = "PASS" if nanoseconds < 1_000_000_000 else "EINTEGRITY"
if nsec_status != case["expected_nsec"]:
raise SystemExit(f"compact nsec oracle mismatch: {case_id}")
elif any(case[key] != "NOT_REACHED" for key in (
"expected_time32", "expected_time64", "expected_nsec"
)) or case["expected_seconds"] is not None:
raise SystemExit(f"compact overflow must stop before conversion: {case_id}")
decoded_compact.append(
{
"id": case_id,
"epoch": epoch,
"delta": delta,
"nanoseconds": nanoseconds,
"mathematical_seconds": mathematical,
"add_status": add_status,
}
)
for case in extended["cases"]:
case_id = case.get("id")
if not isinstance(case_id, str) or case_id in seen_ids:
raise SystemExit(f"invalid or duplicate B13 case id: {case_id!r}")
seen_ids.add(case_id)
seconds = decode_le(case["mtime_le"], 8, signed=True)
nanoseconds = decode_le(case["mtime_nsec_le"], 4, signed=False)
if seconds != case["expected_seconds"]:
raise SystemExit(f"extended signed decode mismatch: {case_id}")
if status_for_range(seconds, int64_min, int64_max) != case["expected_time64"]:
raise SystemExit(f"extended time64 oracle mismatch: {case_id}")
if status_for_range(seconds, int32_min, int32_max) != case["expected_time32"]:
raise SystemExit(f"extended time32 oracle mismatch: {case_id}")
nsec_status = "PASS" if nanoseconds < 1_000_000_000 else "EINTEGRITY"
if nsec_status != case["expected_nsec"]:
raise SystemExit(f"extended nsec oracle mismatch: {case_id}")
decoded_extended.append(
{"id": case_id, "seconds": seconds, "nanoseconds": nanoseconds}
)
fixture_result = {
"status": "PASS",
"compact": decoded_compact,
"extended": decoded_extended,
}
(artifacts / "B13-decoded-fixtures.json").write_text(
json.dumps(fixture_result, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
encoding="ascii",
)
def c_i64(value: int) -> str:
if value == int64_min:
return "INT64_MIN"
if value == int64_max:
return "INT64_MAX"
if value < 0:
return f"(-INT64_C({-value}))"
return f"INT64_C({value})"
def c_status(value: str) -> str:
if value == "PASS":
return "0"
if value == "EINTEGRITY":
return "EINTEGRITY"
raise SystemExit(f"cannot emit non-terminal status: {value}")
program = f'''#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <time.h>
#define EINTEGRITY {eintegrity}
struct erofs_inode {{
\ttime_t mtime;
\tuint32_t mtime_nsec;
}};
{timestamp_helper}
static int
checked_compact(int64_t epoch, uint32_t delta, int64_t *seconds)
{{
\tif (__builtin_add_overflow(epoch, (int64_t)delta, seconds))
\t\treturn (EINTEGRITY);
\treturn (0);
}}
static int
checked_time32(int64_t seconds, int32_t *result)
{{
\treturn (__builtin_add_overflow(seconds, 0, result) ? EINTEGRITY : 0);
}}
static int
checked_time64(int64_t seconds, int64_t *result)
{{
\treturn (__builtin_add_overflow(seconds, 0, result) ? EINTEGRITY : 0);
}}
static int
check_timestamp(const char *id, int64_t seconds, uint32_t nanoseconds,
int expected_time32, int expected_time64, int expected_nsec)
{{
\tstruct erofs_inode inode = {{ 0 }};
\tint32_t time32;
\tint64_t time64;
\tint error, expected;
\tif (checked_time32(seconds, &time32) != expected_time32 ||
\t checked_time64(seconds, &time64) != expected_time64) {{
\t\tfprintf(stderr, "%s: representability\\n", id);
\t\treturn (1);
\t}}
\texpected = expected_nsec != 0 ? expected_nsec :
\t (sizeof(time_t) == sizeof(int32_t) ? expected_time32 : expected_time64);
\terror = erofs_set_timestamp(&inode, seconds, nanoseconds);
\tif (error != expected) {{
\t\tfprintf(stderr, "%s: timestamp errno %d != %d\\n", id, error, expected);
\t\treturn (1);
\t}}
\tif (error == 0 && ((int64_t)inode.mtime != seconds ||
\t inode.mtime_nsec != nanoseconds)) {{
\t\tfprintf(stderr, "%s: timestamp value\\n", id);
\t\treturn (1);
\t}}
\treturn (0);
}}
int
main(void)
{{
\tint64_t seconds;
\tint error;
\tif (sizeof(time_t) != sizeof(int32_t) && sizeof(time_t) != sizeof(int64_t))
\t\treturn (2);
'''
for case, decoded in zip(compact["cases"], decoded_compact, strict=True):
epoch = decoded["epoch"]
delta = decoded["delta"]
expected_add = c_status(case["expected_add"])
program += f'''\terror = checked_compact({c_i64(epoch)}, UINT32_C({delta}), &seconds);
\tif (error != {expected_add}) {{
\t\tfprintf(stderr, "{case['id']}: compact add errno\\n");
\t\treturn (1);
\t}}
'''
if case["expected_add"] == "PASS":
program += f'''\tif (seconds != {c_i64(case['expected_seconds'])} ||
\t check_timestamp("{case['id']}", seconds, UINT32_C({decoded['nanoseconds']}),
\t {c_status(case['expected_time32'])}, {c_status(case['expected_time64'])},
\t {c_status(case['expected_nsec'])}) != 0)
\t\treturn (1);
'''
for case, decoded in zip(extended["cases"], decoded_extended, strict=True):
program += f'''\tif (check_timestamp("{case['id']}", {c_i64(decoded['seconds'])},
\t UINT32_C({decoded['nanoseconds']}), {c_status(case['expected_time32'])},
\t {c_status(case['expected_time64'])}, {c_status(case['expected_nsec'])}) != 0)
\t\treturn (1);
'''
program += f'''\tprintf("B13 boundary PASS compact={len(compact['cases'])} extended={len(extended['cases'])} time_t=%zu\\n",
\t sizeof(time_t) * 8);
\treturn (0);
}}
'''
program_path = artifacts / "B13-time-boundary.c"
binary_path = artifacts / "B13-time-boundary"
program_path.write_text(program, encoding="ascii")
compiled = subprocess.run(
[
"cc", "-std=c11", "-O2", "-Wall", "-Wextra", "-Werror",
str(program_path), "-o", str(binary_path),
],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
(artifacts / "B13-time-boundary.compile.stdout").write_text(
compiled.stdout, encoding="utf-8"
)
(artifacts / "B13-time-boundary.compile.stderr").write_text(
compiled.stderr, encoding="utf-8"
)
if compiled.returncode != 0:
raise SystemExit("B13 boundary extractor compilation failed")
executed = subprocess.run(
[str(binary_path)],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
(artifacts / "B13-time-boundary.stdout").write_text(
executed.stdout, encoding="utf-8"
)
(artifacts / "B13-time-boundary.stderr").write_text(
executed.stderr, encoding="utf-8"
)
if executed.returncode != 0:
raise SystemExit("B13 boundary extractor execution failed")
program32 = program.replace("#include <time.h>\n", "#define time_t int32_t\n", 1)
program32_path = artifacts / "B13-time-boundary32.c"
binary32_path = artifacts / "B13-time-boundary32"
program32_path.write_text(program32, encoding="ascii")
compiled32 = subprocess.run(
[
"cc", "-std=c11", "-O2", "-Wall", "-Wextra", "-Werror",
str(program32_path), "-o", str(binary32_path),
],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
(artifacts / "B13-time-boundary32.compile.stdout").write_text(
compiled32.stdout, encoding="utf-8"
)
(artifacts / "B13-time-boundary32.compile.stderr").write_text(
compiled32.stderr, encoding="utf-8"
)
if compiled32.returncode != 0:
raise SystemExit("B13 32-bit time_t extractor compilation failed")
executed32 = subprocess.run(
[str(binary32_path)],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
(artifacts / "B13-time-boundary32.stdout").write_text(
executed32.stdout, encoding="utf-8"
)
(artifacts / "B13-time-boundary32.stderr").write_text(
executed32.stderr, encoding="utf-8"
)
if executed32.returncode != 0:
raise SystemExit("B13 32-bit time_t extractor execution failed")
result = {
"status": "PASS",
"final_id": "P15-018",
"baseline": baseline,
"compact_cases": len(compact["cases"]),
"extended_cases": len(extended["cases"]),
"signed_add_overflows": sum(
case["expected_add"] == "EINTEGRITY" for case in compact["cases"]
),
"negative_seconds_passes": sum(
case["expected_seconds"] is not None and case["expected_seconds"] < 0
for case in compact["cases"] + extended["cases"]
),
"time32_lower_rejections": sum(
case["expected_seconds"] is not None
and case["expected_seconds"] < int32_min
and case["expected_time32"] == "EINTEGRITY"
for case in compact["cases"] + extended["cases"]
),
"time32_upper_rejections": sum(
case["expected_seconds"] is not None
and case["expected_seconds"] > int32_max
and case["expected_time32"] == "EINTEGRITY"
for case in compact["cases"] + extended["cases"]
),
"nanosecond_rejections": sum(
case["expected_nsec"] == "EINTEGRITY"
for case in compact["cases"] + extended["cases"]
),
"ondisk_header_byte_identical": True,
"ondisk_seconds_width": 64,
"ondisk_compact_delta_width": 32,
"time_t_checked_conversion": True,
"extracted_helper_time_widths": [32, 64],
"unsupported_ckd_api": False,
"positive_errno_preserved": True,
"qemu": "NOT_RUN",
"full_feature_suite": "NOT_RUN",
}
(artifacts / "B13-result.json").write_text(
json.dumps(result, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
encoding="ascii",
)
print(
f"B13 PASS compact={len(compact['cases'])} extended={len(extended['cases'])} "
f"signed-overflow={result['signed_add_overflows']}"
)
PY
then
:
else
pre15_dut_fail 'B13 signed timestamp source or boundary check failed'
fi
sha256sum "$artifacts/B13-overflow-scan.txt" \
"$artifacts/B13-decoded-fixtures.json" \
"$artifacts/B13-time-boundary.c" "$artifacts/B13-time-boundary" \
"$artifacts/B13-time-boundary32.c" "$artifacts/B13-time-boundary32" \
"$artifacts/B13-result.json" > "$artifacts/SHA256SUMS"
printf 'B13 PASS signed timestamps and checked time_t boundaries\n'
+226
View File
@@ -0,0 +1,226 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=bf72cdffbc2b58c2b6468e20391eea14096b5a83
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
generator=$fixture_dir/B14-chunk-fixtures.py
kld_builder=$fixture_dir/B14-build-kld.sh
probe=$fixture_dir/B14-chunk-probe.c
spec=$fixture_dir/B14-chunk-spec.json
artifacts=$PRE15_RUN_DIR/artifacts
first=$PRE15_CASE_TMP/first
second=$PRE15_CASE_TMP/second
for tool in cmp fsck.erofs git mkfs.erofs python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B14 host tool: $tool"
done
pre15_record_fixture b14-generator "$generator"
pre15_record_fixture b14-kld-builder "$kld_builder"
pre15_record_fixture b14-probe "$probe"
pre15_record_fixture b14-spec "$spec"
pre15_record_fixture b14-case "$PRE15_DUT/tests/pre15/cases/B14-chunk-types.sh"
pre15_record_fixture b14-inode "$PRE15_DUT/src/inode.c"
pre15_record_fixture b14-data "$PRE15_DUT/src/data.c"
pre15_record_fixture b14-vnops "$PRE15_DUT/src/erofs_vnops.c"
pre15_record_fixture b14-linux-inode "$PRE15_ROOT/src-linux/inode.c"
mkdir -p "$artifacts"
if ! python3 -B "$generator" generate --output "$first" --spec "$spec" \
>"$artifacts/generate-first.stdout" 2>"$artifacts/generate-first.stderr"; then
pre15_runner_fail 'B14 first fixture generation failed'
fi
if ! python3 -B "$generator" generate --output "$second" --spec "$spec" \
>"$artifacts/generate-second.stdout" 2>"$artifacts/generate-second.stderr"; then
pre15_runner_fail 'B14 repeat fixture generation failed'
fi
if ! cmp "$first/manifest.json" "$second/manifest.json" || \
! cmp "$first/SHA256SUMS" "$second/SHA256SUMS"; then
pre15_runner_fail 'B14 repeat generation is not byte deterministic'
fi
for name in B14-slot1.blob B14-good32.erofs B14-good48.erofs \
B14-bad-index.erofs B14-bad-index48.erofs B14-bad-reserved.erofs \
B14-bad-format.erofs B14-bad-48-no-index.erofs; do
pre15_record_fixture "b14-$name" "$first/$name"
done
pre15_target_reached
if ! python3 -B "$generator" audit --root "$PRE15_ROOT" --dut "$PRE15_DUT" \
--baseline "$baseline" --spec "$spec" --report "$artifacts/B14-source-report.json" \
>"$artifacts/audit.stdout" 2>"$artifacts/audit.stderr"; then
pre15_dut_fail 'B14 source or Linux/FreeBSD semantic audit failed'
fi
if ! python3 -B "$generator" verify --output "$first" --spec "$spec" \
>"$artifacts/verify-first.stdout" 2>"$artifacts/verify-first.stderr"; then
pre15_dut_fail 'B14 fixture oracle failed'
fi
if ! python3 -B "$generator" verify --output "$second" --spec "$spec" \
>"$artifacts/verify-second.stdout" 2>"$artifacts/verify-second.stderr"; then
pre15_runner_fail 'B14 repeat fixture verification failed'
fi
if test "${PRE15_MODE:-host}" != qemu; then
printf '%s\n' 'TC174 host fixture/source oracle PASS' \
'QEMU runtime NOT_RUN in host mode'
exit 0
fi
for tool in awk cc clang file nm scp tar; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B14 QEMU tool: $tool"
done
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
fixture_archive=$PRE15_CASE_TMP/B14-fixtures.tar.gz
module=$PRE15_CASE_TMP/B14-erofs.ko
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
"$PRE15_CASE_TMP/kld-work" >"$artifacts/B14-kld-build.stdout" \
2>"$artifacts/B14-kld-build.stderr"; then
pre15_dut_fail 'B14 cross-target KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/B14-kld-file.txt"
sha256sum "$module" >"$artifacts/B14-kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/B14-kld-nm-u.txt"
tar -C "$first" -czf "$fixture_archive" .
pre15_scp()
{
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
}
if ! pre15_scp "$fixture_archive" /root/B14-fixtures.tar.gz || \
! pre15_scp "$probe" /root/B14-chunk-probe.c || \
! pre15_scp "$module" /root/B14-erofs.ko; then
pre15_infra_blocked 'could not transfer B14 module or fixtures to the guest'
fi
if ! pre15_guest_ssh_bounded \
'rm -rf /root/pre15-b14-fixtures && mkdir /root/pre15-b14-fixtures && tar -xzf /root/B14-fixtures.tar.gz -C /root/pre15-b14-fixtures'; then
pre15_infra_blocked 'could not prepare B14 guest directories'
fi
pre15_guest_ssh_bounded sha256 /root/B14-erofs.ko \
>"$artifacts/B14-guest-module-sha256.txt"
if pre15_guest_ssh_bounded kldstat -n erofs >/dev/null 2>&1; then
pre15_infra_blocked 'FreeBSD guest already has an EROFS module loaded'
fi
if ! pre15_guest_ssh_bounded kldload /root/B14-erofs.ko \
>"$artifacts/B14-kldload.stdout" 2>"$artifacts/B14-kldload.stderr"; then
if grep -q 'module already loaded or in kernel' \
"$artifacts/B14-kldload.stderr"; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
pre15_dut_fail 'B14 exact-source KLD failed to load'
fi
pre15_own_guest_kld erofs 'B14 exact-source KLD'
if ! pre15_guest_ssh_bounded cc -Wall -Wextra -Werror \
-o /root/B14-chunk-probe /root/B14-chunk-probe.c; then
pre15_infra_blocked 'could not compile the B14 guest errno probe'
fi
pre15_attach_md()
{
pre15_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode -f "$1") || \
pre15_dut_fail "could not attach B14 provider: $1"
case "$pre15_md" in
md[0-9]*) ;;
*) pre15_runner_fail "unexpected mdconfig output: $pre15_md" ;;
esac
pre15_md=${pre15_md#md}
pre15_own_guest_md "$pre15_md" "$2"
printf '%s\n' "$pre15_md"
}
pre15_mount_image()
{
pre15_image=$1
pre15_mountpoint=$2
pre15_label=$3
pre15_primary=$(pre15_attach_md "/root/pre15-b14-fixtures/$pre15_image" \
"$pre15_label primary")
pre15_guest_ssh_bounded mkdir -p "$pre15_mountpoint"
if ! pre15_guest_ssh_bounded mount -t erofs -o ro \
-o "device.1=/dev/md$blob_md" "/dev/md$pre15_primary" \
"$pre15_mountpoint"; then
pre15_dut_fail "$pre15_label mount failed"
fi
pre15_own_guest_mount "$pre15_mountpoint" "$pre15_label mount"
}
blob_md=$(pre15_attach_md /root/pre15-b14-fixtures/B14-slot1.blob \
'B14 external slot 1')
pre15_mount_image B14-good32.erofs /mnt/pre15-b14-good32 'B14 good32'
pre15_guest_ssh_bounded cmp /root/pre15-b14-fixtures/source/real-dir/entry.txt \
/mnt/pre15-b14-good32/chunk-dir/entry.txt
pre15_guest_ssh_bounded cmp /root/pre15-b14-fixtures/source/target.txt \
/mnt/pre15-b14-good32/chunk-link
pre15_guest_ssh_bounded /root/B14-chunk-probe readlink-pass \
/mnt/pre15-b14-good32/chunk-link target.txt
pre15_guest_ssh_bounded stat -f '%HT %z' /mnt/pre15-b14-good32/chunk-char \
>"$artifacts/B14-char-stat.txt"
pre15_guest_ssh_bounded /root/B14-chunk-probe open-unsupported \
/mnt/pre15-b14-good32/chunk-char
pre15_guest_ssh_bounded ls -1A /mnt/pre15-b14-good32/chunk-dir \
>"$artifacts/B14-directory-list.txt"
if test "$(cat "$artifacts/B14-directory-list.txt")" != entry.txt; then
pre15_dut_fail 'chunk directory readdir returned the wrong set'
fi
pre15_mount_image B14-good48.erofs /mnt/pre15-b14-good48 'B14 good48'
pre15_guest_ssh_bounded cmp /root/pre15-b14-fixtures/source/real-dir/entry.txt \
/mnt/pre15-b14-good48/chunk-dir/entry.txt
pre15_guest_ssh_bounded /root/B14-chunk-probe readlink-pass \
/mnt/pre15-b14-good48/chunk-link target.txt
pre15_mount_image B14-bad-index.erofs /mnt/pre15-b14-bad-index 'B14 bad index'
pre15_guest_ssh_bounded /root/B14-chunk-probe readlink-integrity \
/mnt/pre15-b14-bad-index/chunk-link
pre15_mount_image B14-bad-index48.erofs /mnt/pre15-b14-bad-index48 \
'B14 bad 48-bit index'
pre15_guest_ssh_bounded /root/B14-chunk-probe readlink-integrity \
/mnt/pre15-b14-bad-index48/chunk-link
pre15_mount_image B14-bad-reserved.erofs /mnt/pre15-b14-bad-reserved \
'B14 bad reserved'
pre15_guest_ssh_bounded /root/B14-chunk-probe stat-integrity \
/mnt/pre15-b14-bad-reserved/chunk-dir
pre15_mount_image B14-bad-format.erofs /mnt/pre15-b14-bad-format \
'B14 bad format'
pre15_guest_ssh_bounded /root/B14-chunk-probe stat-unsupported \
/mnt/pre15-b14-bad-format/chunk-link
pre15_mount_image B14-bad-48-no-index.erofs /mnt/pre15-b14-bad-48-no-index \
'B14 bad 48-bit without indexes'
pre15_guest_ssh_bounded /root/B14-chunk-probe stat-integrity \
/mnt/pre15-b14-bad-48-no-index/chunk-link
if pre15_guest_ssh_bounded truss -f -o /root/B14-md-ebusy.truss \
mdconfig -d -u "$blob_md"; then
pre15_dut_fail 'external GEOM provider detached while B14 mounts were active'
fi
if ! pre15_guest_ssh_bounded grep -Eq 'ERR#16' /root/B14-md-ebusy.truss; then
pre15_dut_fail 'external GEOM detach did not return exact EBUSY'
fi
pre15_guest_ssh_bounded cat /root/B14-md-ebusy.truss \
>"$artifacts/B14-md-ebusy.truss"
pre15_guest_ssh_bounded dmesg >"$artifacts/B14-dmesg.txt"
printf '%s\n' 'TC174 QEMU directory/symlink/multidevice/48-bit PASS'
+343
View File
@@ -0,0 +1,343 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
spec=$fixture_dir/B16-symlink-spec.json
probe=$fixture_dir/B16-symlink-probe.c
kld_builder=$fixture_dir/B16-build-kld.sh
gate_script=$PRE15_DUT/tests/pre15/gates/P15-019.sh
gate_input=$PRE15_DUT/tests/pre15/gates/P15-019-input.json
artifacts=$PRE15_RUN_DIR/artifacts
gate_clone=$PRE15_CASE_TMP/gate-clone
first=$PRE15_CASE_TMP/first
second=$PRE15_CASE_TMP/second
for tool in cmp fsck.erofs git mkfs.erofs python3 sha256sum timeout; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B16 host tool: $tool"
done
pre15_record_fixture b16-spec "$spec"
pre15_record_fixture b16-probe "$probe"
pre15_record_fixture b16-kld-builder "$kld_builder"
pre15_record_fixture b16-case "$PRE15_DUT/tests/pre15/cases/B16-symlink.sh"
pre15_record_fixture b16-gate "$gate_script"
pre15_record_fixture b16-gate-input "$gate_input"
pre15_record_fixture b16-internal "$PRE15_DUT/src/internal.h"
pre15_record_fixture b16-inode "$PRE15_DUT/src/inode.c"
pre15_record_fixture b16-data "$PRE15_DUT/src/data.c"
pre15_record_fixture b16-vnops "$PRE15_DUT/src/erofs_vnops.c"
pre15_record_fixture b16-linux-inode "$PRE15_ROOT/src-linux/inode.c"
mkdir -p "$artifacts"
if ! git clone -q --shared --no-checkout "$PRE15_ROOT" "$gate_clone" \
>"$artifacts/gate-clone.stdout" 2>"$artifacts/gate-clone.stderr" || \
! git -C "$gate_clone" checkout -q --detach \
aea34aba38a298d493d0a584f3985cf3589f358e \
>"$artifacts/gate-checkout.stdout" 2>"$artifacts/gate-checkout.stderr"; then
pre15_runner_fail 'could not materialize the frozen P15-019 gate commit'
fi
frozen_gate=$gate_clone/repo-pre-15/tests/pre15/gates/P15-019.sh
if ! timeout -k 10 240 "$frozen_gate" \
--base d645feb720c7022d2138d2a62eb72c022eb75351 --output "$first" \
>"$artifacts/gate-first.stdout" 2>"$artifacts/gate-first.stderr"; then
pre15_runner_fail 'first frozen P15-019 gate replay failed'
fi
if ! timeout -k 10 240 "$frozen_gate" \
--base d645feb720c7022d2138d2a62eb72c022eb75351 --output "$second" \
>"$artifacts/gate-second.stdout" 2>"$artifacts/gate-second.stderr"; then
pre15_runner_fail 'second frozen P15-019 gate replay failed'
fi
for name in result.json cases.json commands.json semantics.json cleanup.json \
fixtures/SHA256SUMS; do
if ! cmp "$first/host/$name" "$second/host/$name"; then
pre15_runner_fail "P15-019 repeat output differs: $name"
fi
done
if ! python3 -B - "$spec" "$gate_script" "$gate_input" \
"$first/host/result.json" "$first/host/cases.json" \
"$first/host/fixtures/SHA256SUMS" \
"$PRE15_DUT/src/internal.h" "$PRE15_DUT/src/inode.c" \
"$PRE15_DUT/src/data.c" "$PRE15_DUT/src/erofs_vnops.c" \
"$PRE15_ROOT/src-linux/inode.c" \
>"$artifacts/B16-host-audit.json" <<'PY'
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import re
import sys
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def function(source: str, name: str) -> str:
match = re.search(rf"\n{name}\([^;]*?\n\{{", source, re.DOTALL)
if match is None:
raise SystemExit(f"missing function: {name}")
start = match.start() + 1
brace = source.index("{", match.start())
depth = 0
for index in range(brace, len(source)):
if source[index] == "{":
depth += 1
elif source[index] == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
raise SystemExit(f"unterminated function: {name}")
spec = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
gate = Path(sys.argv[2])
gate_input = Path(sys.argv[3])
result = json.loads(Path(sys.argv[4]).read_text(encoding="ascii"))
cases = json.loads(Path(sys.argv[5]).read_text(encoding="ascii"))
sums = Path(sys.argv[6])
internal = Path(sys.argv[7]).read_text(encoding="utf-8")
inode = Path(sys.argv[8]).read_text(encoding="utf-8")
data = Path(sys.argv[9]).read_text(encoding="utf-8")
vnops = Path(sys.argv[10]).read_text(encoding="utf-8")
linux = Path(sys.argv[11]).read_text(encoding="utf-8")
if spec.get("schema") != 1 or spec.get("batch") != "B16" or spec.get("test") != "TC166-symlink-layouts":
raise SystemExit("B16 spec identity changed")
if sha256(gate) != spec["gate"]["script_sha256"] or sha256(gate_input) != spec["gate"]["input_sha256"]:
raise SystemExit("tracked P15-019 gate differs from the GO decision")
if result.get("decision") != "GO" or result.get("case_count") != 35:
raise SystemExit("frozen P15-019 gate did not reproduce GO/35")
if result.get("fixture_set_sha256") != spec["fixture_set_sha256"] or sha256(sums) != spec["fixture_set_sha256"]:
raise SystemExit("B16 fixture-set hash differs from G03")
expected_pairs = {
(layout, case)
for layout in spec["layouts"]
for case in spec["cases"]
}
actual_pairs = {(item["layout"], item["case"]) for item in cases}
if actual_pairs != expected_pairs:
raise SystemExit("B16 fixture matrix is incomplete")
for item in cases:
layout = spec["layouts"][item["layout"]]
if item["inode"]["layout"] != layout["layout"]:
raise SystemExit("B16 fixture layout differs")
validator = function(data, "erofs_validate_symlink_target")
readlink = function(data, "erofs_readlink_target")
read_inode = function(inode, "erofs_read_inode")
markers = (
"vi->vtype != VLNK",
"vi->size == 0",
"vi->size > MAXPATHLEN",
"erofs_read_data(sbi, vi, 0, (size_t)vi->size, &target)",
"memchr(target, '\\0', (size_t)vi->size)",
"erofs_brelse(target)",
)
if not all(marker in validator for marker in markers):
raise SystemExit("B16 bounded validator is incomplete")
if "uiomove" in validator:
raise SystemExit("B16 validator performs partial caller transfer")
if not (
read_inode.index("vi->size == 0")
< read_inode.index("z_erofs_fill_inode(sbi, vi)")
and read_inode.index("vi->size > MAXPATHLEN")
< read_inode.index("z_erofs_fill_inode(sbi, vi)")
and read_inode.index("erofs_validate_symlink_target(sbi, vi)")
> read_inode.index("z_erofs_fill_inode(sbi, vi)")
):
raise SystemExit("B16 size/content validation order changed")
if "erofs_validate_symlink_target" not in internal:
raise SystemExit("B16 validator prototype is missing")
if "vi->size == 0" not in readlink or "vi->size > MAXPATHLEN" not in readlink or "erofs_read_uio" not in readlink:
raise SystemExit("B16 VOP readlink guard/path changed")
if "return (erofs_readlink_target(ap->a_vp, ap->a_uio));" not in vnops:
raise SystemExit("FreeBSD VOP_READLINK adapter changed")
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", internal + inode + data + vnops):
raise SystemExit("B16 introduced Linux negative errno")
if "i_link" in internal + inode + data + vnops or "page_get_link" in internal + inode + data + vnops:
raise SystemExit("B16 copied the Linux page/i_link implementation")
for marker in (
"vi->datalayout == EROFS_INODE_FLAT_INLINE",
"kmemdup_nul(bptr + ofs, inode->i_size, GFP_KERNEL)",
".get_link = page_get_link",
".get_link = simple_get_link",
"return -EFSCORRUPTED",
):
if marker not in linux:
raise SystemExit("Linux symlink semantic anchor changed")
report = {
"status": "PASS",
"candidate": "P15-019",
"test": "TC166-symlink-layouts",
"fixture_count": len(cases),
"fixture_set_sha256": result["fixture_set_sha256"],
"layouts": sorted(spec["layouts"]),
"errno": spec["errno"],
"linux_path": "fast inline i_link validation; page_get_link otherwise; negative errno",
"freebsd_path": "vnode VOP_READLINK; bounded pre-publication scan; GEOM/map; positive errno",
}
print(json.dumps(report, indent=2, sort_keys=True))
PY
then
pre15_runner_fail 'B16 independent fixture/source audit failed'
fi
cp "$first/host/result.json" "$artifacts/P15-019-result.json"
cp "$first/host/cases.json" "$artifacts/P15-019-cases.json"
cp "$first/host/fixtures/SHA256SUMS" "$artifacts/B16-SHA256SUMS"
pre15_record_fixture b16-generated-sums "$artifacts/B16-SHA256SUMS"
pre15_target_reached
if test "${PRE15_MODE:-host}" != qemu; then
printf '%s\n' \
'TC166 host all-layout fixture and independent source oracle PASS' \
'QEMU runtime NOT_RUN in host mode'
exit 0
fi
for tool in awk cc clang file nm scp tar; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B16 QEMU tool: $tool"
done
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
fixture_archive=$PRE15_CASE_TMP/B16-fixtures.tar.gz
module=$PRE15_CASE_TMP/B16-erofs.ko
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
"$PRE15_CASE_TMP/kld-work" >"$artifacts/B16-kld-build.stdout" \
2>"$artifacts/B16-kld-build.stderr"; then
pre15_dut_fail 'B16 cross-target KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/B16-kld-file.txt"
sha256sum "$module" >"$artifacts/B16-kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/B16-kld-nm-u.txt"
tar -C "$first/host/fixtures" -czf "$fixture_archive" .
pre15_scp()
{
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
}
if ! pre15_scp "$fixture_archive" /root/B16-fixtures.tar.gz || \
! pre15_scp "$probe" /root/B16-symlink-probe.c || \
! pre15_scp "$module" /root/B16-erofs.ko; then
pre15_infra_blocked 'could not transfer B16 module or fixtures to the guest'
fi
if ! pre15_guest_ssh_bounded \
'rm -rf /root/pre15-b16-fixtures && mkdir /root/pre15-b16-fixtures && tar -xzf /root/B16-fixtures.tar.gz -C /root/pre15-b16-fixtures'; then
pre15_infra_blocked 'could not prepare B16 guest fixtures'
fi
if pre15_guest_ssh_bounded kldstat -n erofs >/dev/null 2>&1; then
pre15_infra_blocked 'FreeBSD guest already has an EROFS module loaded'
fi
if ! pre15_guest_ssh_bounded kldload /root/B16-erofs.ko \
>"$artifacts/B16-kldload.stdout" 2>"$artifacts/B16-kldload.stderr"; then
if grep -q 'module already loaded or in kernel' "$artifacts/B16-kldload.stderr"; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
pre15_dut_fail 'B16 exact-source KLD failed to load'
fi
pre15_own_guest_kld erofs 'B16 exact-source KLD'
if ! pre15_guest_ssh_bounded cc -std=c11 -Wall -Wextra -Werror \
-o /root/B16-symlink-probe /root/B16-symlink-probe.c; then
pre15_infra_blocked 'could not compile the B16 guest probe'
fi
pre15_attach_md()
{
pre15_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode -f "$1") || \
pre15_dut_fail "could not attach B16 provider: $1"
case "$pre15_md" in
md[0-9]*) ;;
*) pre15_runner_fail "unexpected mdconfig output: $pre15_md" ;;
esac
pre15_md=${pre15_md#md}
pre15_own_guest_md "$pre15_md" "$2"
printf '%s\n' "$pre15_md"
}
pre15_mount_case()
{
pre15_layout=$1
pre15_case=$2
pre15_mountpoint=/mnt/pre15-b16-$pre15_layout-$pre15_case
pre15_options=
if test "$pre15_layout" = chunk; then
pre15_blob=$(pre15_attach_md \
"/root/pre15-b16-fixtures/$pre15_layout-$pre15_case.blob" \
"B16 $pre15_layout $pre15_case blob")
pre15_options="-o device.1=/dev/md$pre15_blob"
fi
pre15_primary=$(pre15_attach_md \
"/root/pre15-b16-fixtures/$pre15_layout-$pre15_case.erofs" \
"B16 $pre15_layout $pre15_case primary")
pre15_guest_ssh_bounded mkdir -p "$pre15_mountpoint"
if ! pre15_guest_ssh_bounded mount -t erofs -o ro $pre15_options \
"/dev/md$pre15_primary" "$pre15_mountpoint"; then
pre15_dut_fail "B16 $pre15_layout $pre15_case mount failed"
fi
pre15_own_guest_mount "$pre15_mountpoint" \
"B16 $pre15_layout $pre15_case mount"
printf '%s\n' "$pre15_mountpoint/link"
}
for layout in chunk compressed fragment inline plain; do
case "$layout" in
compressed) short_length=64 ;;
*) short_length=31 ;;
esac
for case_id in normal-short normal-max empty nul-middle too-long; do
path=$(pre15_mount_case "$layout" "$case_id")
case "$case_id" in
normal-short)
pre15_guest_ssh_bounded /root/B16-symlink-probe \
readlink-pass "$path" "$short_length"
pre15_guest_ssh_bounded /root/B16-symlink-probe \
concurrent "$path" "$short_length" 16 20
;;
normal-max)
pre15_guest_ssh_bounded /root/B16-symlink-probe \
readlink-pass "$path" 1024
pre15_guest_ssh_bounded /root/B16-symlink-probe \
follow-errno "$path" 63
;;
empty|nul-middle)
pre15_guest_ssh_bounded /root/B16-symlink-probe \
readlink-errno "$path" 97
;;
too-long)
pre15_guest_ssh_bounded /root/B16-symlink-probe \
readlink-errno "$path" 63
;;
esac
done
done
pre15_guest_ssh_bounded dmesg >"$artifacts/B16-dmesg.txt"
if grep -Eq 'panic:|Fatal trap|lock order reversal|KDB: stack backtrace' \
"$artifacts/B16-dmesg.txt"; then
pre15_dut_fail 'B16 runtime produced a kernel diagnostic'
fi
printf '%s\n' 'TC166 QEMU all-layout symlink validation PASS'
+275
View File
@@ -0,0 +1,275 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
fixtures_only=0
if test "${1:-}" = --fixtures-only; then
fixtures_only=1
shift
fi
test "$#" -eq 0 || pre15_runner_fail 'B17 accepts only --fixtures-only'
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
spec=$fixture_dir/B17-xattr-spec.json
generator=$fixture_dir/B17-xattr-generate.py
oracle=$fixture_dir/B17-xattr-oracle.py
seed=$fixture_dir/B17-xattr-seed.erofs
artifacts=$PRE15_RUN_DIR/artifacts
first=$PRE15_CASE_TMP/first
second=$PRE15_CASE_TMP/second
rebuilt=$PRE15_CASE_TMP/rebuilt-seed.erofs
rebuild_work=$PRE15_CASE_TMP/rebuild-work
for tool in fsck.erofs git mkfs.erofs python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B17 host tool: $tool"
done
pre15_record_fixture b17-spec "$spec"
pre15_record_fixture b17-generator "$generator"
pre15_record_fixture b17-oracle "$oracle"
pre15_record_fixture b17-seed "$seed"
pre15_record_fixture b17-case "$PRE15_DUT/tests/pre15/cases/B17-xattr-integrity.sh"
mkdir -p "$artifacts"
if python3 -B "$generator" rebuild-seed --spec "$spec" --seed "$seed" \
--output "$rebuilt" --work "$rebuild_work" \
>"$artifacts/rebuild-seed.json" 2>"$artifacts/rebuild-seed.stderr"; then
:
else
pre15_runner_fail 'B17 tracked seed is not reproducible'
fi
if python3 -B "$generator" generate --spec "$spec" --seed "$seed" \
--output "$first" >"$artifacts/generate-first.json" \
2>"$artifacts/generate-first.stderr"; then
:
else
pre15_runner_fail 'B17 first fixture generation failed'
fi
if python3 -B "$generator" generate --spec "$spec" --seed "$seed" \
--output "$second" >"$artifacts/generate-second.json" \
2>"$artifacts/generate-second.stderr"; then
:
else
pre15_runner_fail 'B17 second fixture generation failed'
fi
if cmp "$first/fixture-index.json" "$second/fixture-index.json" && \
find "$first" -type f ! -name fixture-index.json -printf '%f\n' | sort | \
while IFS= read -r name; do cmp "$first/$name" "$second/$name" || exit 1; done
then
:
else
pre15_runner_fail 'B17 fixture generation is not byte reproducible'
fi
if python3 -B "$oracle" --spec "$spec" --fixtures "$first" \
--report "$artifacts/oracle-first.json" \
>"$artifacts/oracle-first.stdout" 2>"$artifacts/oracle-first.stderr"; then
:
else
pre15_runner_fail 'B17 independent first oracle replay failed'
fi
if python3 -B "$oracle" --spec "$spec" --fixtures "$second" \
--report "$artifacts/oracle-second.json" \
>"$artifacts/oracle-second.stdout" 2>"$artifacts/oracle-second.stderr"; then
:
else
pre15_runner_fail 'B17 independent second oracle replay failed'
fi
cp "$first/fixture-index.json" "$artifacts/B17-fixture-index.json"
pre15_record_fixture b17-generated-index "$artifacts/B17-fixture-index.json"
if python3 - "$artifacts/B17-fixture-index.json" \
"$artifacts/oracle-first.json" "$artifacts/B17-matrix.tsv" <<'PY'
import json
from pathlib import Path
import sys
index = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
report = json.loads(Path(sys.argv[2]).read_text(encoding="ascii"))
if index["fixture_count"] != 25 or index["legal_count"] != 10 or index["damaged_count"] != 15:
raise SystemExit("B17 fixture cardinality changed")
if report["status"] != "PASS" or report["legal_passed"] != 10 or report["damaged_passed"] != 15:
raise SystemExit("B17 oracle matrix is incomplete")
lines = ["id\tclass\terrno\treject"]
for item in report["results"]:
lines.append(
f"{item['id']}\t{item['class']}\t{item['actual_errno']}\t{item['actual_reject']}"
)
Path(sys.argv[3]).write_text("\n".join(lines) + "\n", encoding="ascii")
print(
f"B17 fixtures: {index['fixture_count']} "
f"({index['legal_count']} legal, {index['damaged_count']} damaged)"
)
print(f"B17 fixture-set SHA256: {index['fixture_set_sha256']}")
PY
then
:
else
pre15_runner_fail 'B17 fixture matrix summary failed'
fi
pre15_target_reached
if test "$fixtures_only" -eq 1; then
printf '%s\n' 'B17 fixture gate: READY'
exit 0
fi
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$artifacts/B17-source-check.json" <<'PY'
from pathlib import Path
import json
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
output = Path(sys.argv[3])
baseline = "98d76e4e73ed9a760b7f684d453b301c73167c22"
def committed(name: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/src/{name}"],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
raise SystemExit(f"cannot read B17 baseline {name}: {completed.stderr}")
return completed.stdout
def replace_once(source: str, old: str, new: str, label: str) -> str:
count = source.count(old)
if count != 1:
raise SystemExit(f"{label}: expected one baseline transform, found {count}")
return source.replace(old, new, 1)
current = {
name: (dut / "src" / name).read_text(encoding="utf-8")
for name in ("internal.h", "super.c", "xattr.c")
}
base = {name: committed(name) for name in current}
expected_internal = replace_once(
base["internal.h"],
"\tuint8_t xattr_prefix_count;\n",
"\tuint8_t xattr_prefix_count;\n\tuint8_t xattr_filter_reserved;\n",
"raw xattr filter state",
)
expected_super = replace_once(
base["super.c"],
"\t/* A non-zero reserved value disables the current name-filter format. */\n"
"\tif (erofs_sb_has_xattr_filter(sbi) && dsb->xattr_filter_reserved != 0)\n"
"\t\tsbi->feature_compat &= ~EROFS_FEATURE_COMPAT_XATTR_FILTER;\n",
"\t/* Preserve the raw feature declaration and gate its format at use sites. */\n"
"\tsbi->xattr_filter_reserved = dsb->xattr_filter_reserved;\n",
"raw feature preservation",
)
expected_xattr = replace_once(
base["xattr.c"],
"\tif (entry_size > remaining)\n\t\treturn (EINTEGRITY);\n",
"\tif (entry_size > remaining)\n\t\treturn (EINTEGRITY);\n"
"\tif (memchr(entry->e_name, '\\0', entry->e_name_len) != NULL)\n"
"\t\treturn (EINTEGRITY);\n",
"inline name NUL validation",
)
expected_xattr = replace_once(
expected_xattr,
"\terror = erofs_xattr_read_backing(sbi, backing_en, off, entry_size,\n"
"\t entrybuf);\n"
"\tif (error != 0)\n"
"\t\treturn (error);\n"
"\tif (entry_sizep != NULL)\n",
"\terror = erofs_xattr_read_backing(sbi, backing_en, off, entry_size,\n"
"\t entrybuf);\n"
"\tif (error != 0)\n"
"\t\treturn (error);\n"
"\terror = erofs_xattr_validate_entry(entrybuf->data, entry_size, NULL,\n"
"\t value_sizep);\n"
"\tif (error != 0) {\n"
"\t\terofs_put_metabuf(entrybuf);\n"
"\t\treturn (error);\n"
"\t}\n"
"\tif (entry_sizep != NULL)\n",
"shared name NUL validation",
)
expected_xattr = replace_once(
expected_xattr,
"\tif (!erofs_sb_has_xattr_filter(sbi))\n\t\treturn (0);\n",
"\tif (!erofs_sb_has_xattr_filter(sbi) ||\n"
"\t sbi->xattr_filter_reserved != 0)\n"
"\t\treturn (0);\n",
"xattr filter use-site gate",
)
expected_xattr = replace_once(
expected_xattr,
"\t\tprefix = buf.data;\n"
"\t\tinfix_len = len - sizeof(*prefix);\n"
"\t\tsbi->xattr_prefixes[i].base_index = prefix->base_index;\n",
"\t\tprefix = buf.data;\n"
"\t\tinfix_len = len - sizeof(*prefix);\n"
"\t\tif (memchr(prefix->infix, '\\0', infix_len) != NULL) {\n"
"\t\t\terror = EINTEGRITY;\n"
"\t\t\tgoto fail;\n"
"\t\t}\n"
"\t\tsbi->xattr_prefixes[i].base_index = prefix->base_index;\n",
"long-prefix NUL validation",
)
expected = {
"internal.h": expected_internal,
"super.c": expected_super,
"xattr.c": expected_xattr,
}
for name in expected:
if current[name] != expected[name]:
raise SystemExit(f"{name} differs from the exact B17 ledger transforms")
diff = subprocess.run(
["git", "-C", str(root), "diff", baseline, "--", "repo-pre-15/src/internal.h",
"repo-pre-15/src/super.c", "repo-pre-15/src/xattr.c"],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout
for forbidden in ("xxh32", "bloom", "cache owner", "cache waiter"):
if forbidden in diff.lower():
raise SystemExit(f"B17 source diff contains out-of-scope marker: {forbidden}")
if "return (-E" in "".join(current.values()):
raise SystemExit("negative errno entered the FreeBSD B17 source")
result = {
"status": "PASS",
"baseline": baseline,
"changed_sources": sorted(expected),
"raw_feature_preserved": True,
"reserved_gated_at_use": True,
"inline_name_nul": True,
"shared_name_nul": True,
"prefix_infix_nul": True,
"acl_empty_suffix_preserved": True,
}
output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="ascii")
print("B17 source: exact raw-feature and name-integrity transforms present")
PY
then
:
else
pre15_dut_fail 'B17 source does not match the fixture-authorized transforms'
fi
printf '%s\n' 'B17 source gate: PASS'
+359
View File
@@ -0,0 +1,359 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=145d892afc8a425294029d4349f03f1b17cf8f92
artifacts=$PRE15_RUN_DIR/artifacts
xattr=$PRE15_DUT/src/xattr.c
linux_xattr=$PRE15_ROOT/src-linux/xattr.c
xattr_header=$PRE15_DUT/src/xattr.h
vnops=$PRE15_DUT/src/erofs_vnops.c
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
fixture_spec=$fixture_dir/B17-xattr-spec.json
fixture_generator=$fixture_dir/B17-xattr-generate.py
fixture_oracle=$fixture_dir/B17-xattr-oracle.py
fixture_seed=$fixture_dir/B17-xattr-seed.erofs
for tool in git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B18 host tool: $tool"
done
pre15_record_fixture b18-freebsd-xattr "$xattr"
pre15_record_fixture b18-linux-xattr "$linux_xattr"
pre15_record_fixture b18-xattr-header "$xattr_header"
pre15_record_fixture b18-vnops "$vnops"
pre15_record_fixture b18-b17-spec "$fixture_spec"
pre15_record_fixture b18-b17-generator "$fixture_generator"
pre15_record_fixture b18-b17-oracle "$fixture_oracle"
pre15_record_fixture b18-b17-seed "$fixture_seed"
pre15_record_fixture b18-case \
"$PRE15_DUT/tests/pre15/cases/B18-xattr-order.sh"
mkdir -p "$artifacts"
pre15_target_reached
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \
"$artifacts/B18-xattr-order.json" <<'PY'
from __future__ import annotations
from collections import Counter
import hashlib
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
baseline = sys.argv[3]
output = Path(sys.argv[4])
xattr_path = dut / "src/xattr.c"
current = xattr_path.read_text(encoding="utf-8")
linux = (root / "src-linux/xattr.c").read_text(encoding="utf-8")
def committed_bytes(path: str) -> bytes:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
raise SystemExit(
f"cannot read B18 baseline {path}: "
f"{completed.stderr.decode(errors='replace')}"
)
return completed.stdout
def require_once(source: str, marker: str, label: str) -> int:
count = source.count(marker)
if count != 1:
raise SystemExit(f"{label}: expected one marker, found {count}: {marker!r}")
return source.index(marker)
def extract_function(source: str, name: str) -> str:
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if not match:
raise SystemExit(f"missing function: {name}")
name_line = source.rfind("\n", 0, match.start()) + 1
start = source.rfind("\n", 0, name_line - 1) + 1
brace = source.find("{", match.end())
if brace < 0:
raise SystemExit(f"missing function body: {name}")
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
raise SystemExit(f"unterminated function: {name}")
def require_order(source: str, markers: tuple[str, ...], label: str) -> None:
position = -1
for marker in markers:
position = source.find(marker, position + 1)
if position < 0:
raise SystemExit(f"{label}: missing ordered marker: {marker}")
base = committed_bytes("src/xattr.c").decode("utf-8")
iterator_start = require_once(base, "struct erofs_xattr_iter {", "iterator")
iterator_end_marker = "\n};\n\n"
iterator_end = base.find(iterator_end_marker, iterator_start)
if iterator_end < 0:
raise SystemExit("B18 baseline iterator terminator is absent")
iterator_end += len(iterator_end_marker)
iterator_block = base[iterator_start:iterator_end]
without_iterator = base[:iterator_start] + base[iterator_end:]
acl_start = require_once(
without_iterator,
"static int\nerofs_inode_has_noacl(",
"ACL/filter block",
)
acl_end = require_once(
without_iterator,
"static int\nerofs_listxattr_foreach(",
"common iterator block",
)
if acl_end <= acl_start:
raise SystemExit("B18 baseline ACL/filter block is not before the iterator core")
acl_block = without_iterator[acl_start:acl_end]
expected = without_iterator[:acl_start] + without_iterator[acl_end:]
core_start = require_once(
expected,
"static int\nerofs_xattr_backing_size(",
"backing core",
)
expected = expected[:core_start] + iterator_block + expected[core_start:]
acl_adapter = require_once(expected, "int\nerofs_get_acl(", "ACL adapter")
expected = expected[:acl_adapter] + acl_block + expected[acl_adapter:]
if current != expected:
raise SystemExit("xattr.c differs from the two exact B18 definition moves")
base_names = re.findall(r"^(erofs_[A-Za-z0-9_]+)\s*\(", base, re.MULTILINE)
current_names = re.findall(
r"^(erofs_[A-Za-z0-9_]+)\s*\(", current, re.MULTILINE
)
if Counter(base_names) != Counter(current_names):
raise SystemExit("B18 changed the xattr function definition multiset")
base_functions = {name: extract_function(base, name) for name in base_names}
current_functions = {name: extract_function(current, name) for name in current_names}
for name in base_functions:
if current_functions[name] != base_functions[name]:
raise SystemExit(f"B18 changed function text instead of moving it: {name}")
if current_functions[name].startswith("static ") != base_functions[name].startswith(
"static "
):
raise SystemExit(f"B18 changed symbol visibility: {name}")
def direct_calls(functions: dict[str, str]) -> Counter[tuple[str, str]]:
calls: Counter[tuple[str, str]] = Counter()
for caller, function in functions.items():
brace = function.find("{")
for callee in re.findall(
r"\b(erofs_[A-Za-z0-9_]+)\s*\(", function[brace + 1 :]
):
calls[(caller, callee)] += 1
return calls
base_calls = direct_calls(base_functions)
current_calls = direct_calls(current_functions)
if current_calls != base_calls:
raise SystemExit("B18 changed the direct-call multiset")
expected_order = [
"erofs_xattr_backing_size",
"erofs_xattr_read_backing",
"erofs_xattr_read_metadata",
"erofs_xattr_move",
"erofs_xattr_load_body",
"erofs_xattr_validate_entry",
"erofs_xattr_prefix",
"erofs_xattr_namespace_prefix",
"erofs_xattr_list_move",
"erofs_xattr_resolve_name",
"erofs_xattr_name_match",
"erofs_xattr_shared_entry_offset",
"erofs_xattr_load_shared_entry",
"erofs_listxattr_foreach",
"erofs_getxattr_foreach",
"erofs_xattr_iter_inline",
"erofs_xattr_iter_shared",
"erofs_getxattr",
"erofs_listxattr",
"erofs_xattr_prefixes_cleanup",
"erofs_xattr_prefixes_init",
"erofs_inode_has_noacl",
"erofs_acl_from_mode",
"erofs_posix_acl_from_xattr",
"erofs_get_acl",
]
if current_names != expected_order:
raise SystemExit(f"B18 xattr definition order mismatch: {current_names}")
if current.index("struct erofs_xattr_iter {") > current.index(
"erofs_xattr_backing_size("
):
raise SystemExit("xattr iterator contract is not before the backing/core helpers")
linux_aligned_core = (
"erofs_listxattr_foreach(",
"erofs_getxattr_foreach(",
"erofs_xattr_iter_inline(",
"erofs_xattr_iter_shared(",
"erofs_getxattr(",
"erofs_listxattr(",
"erofs_xattr_prefixes_cleanup(",
"erofs_xattr_prefixes_init(",
)
require_order(current, linux_aligned_core, "FreeBSD common xattr core")
require_order(linux, linux_aligned_core, "Linux common xattr core")
readonly_paths = (
"src/xattr.h",
"src/erofs_vnops.c",
"tests/pre15/fixtures/B17-xattr-spec.json",
"tests/pre15/fixtures/B17-xattr-generate.py",
"tests/pre15/fixtures/B17-xattr-oracle.py",
"tests/pre15/fixtures/B17-xattr-seed.erofs",
)
for path in readonly_paths:
if (dut / path).read_bytes() != committed_bytes(path):
raise SystemExit(f"B18 changed a read-only xattr/VOP/fixture contract: {path}")
vnops = (dut / "src/erofs_vnops.c").read_text(encoding="utf-8")
getextattr = extract_function(vnops, "erofs_getextattr")
listextattr = extract_function(vnops, "erofs_listextattr")
require_order(
getextattr,
("extattr_check_cred(", "switch (ap->a_attrnamespace)", "erofs_getxattr("),
"FreeBSD getextattr adapter",
)
require_order(
listextattr,
("extattr_check_cred(", "switch (ap->a_attrnamespace)", "erofs_listxattr("),
"FreeBSD listextattr adapter",
)
if vnops.count("extattr_check_cred(") != 2:
raise SystemExit("FreeBSD extattr credential boundary changed")
if "ERANGE" in current:
raise SystemExit("Linux all-or-nothing xattr buffer semantics entered FreeBSD")
if "uiomove(value, value_size, uio)" not in current:
raise SystemExit("FreeBSD partial extattr transfer path is absent")
if re.search(r"return\s*(?:\(\s*)?-E[A-Z0-9_]+", current):
raise SystemExit("Linux negative errno entered FreeBSD xattr.c")
load_body = extract_function(current, "erofs_xattr_load_body")
require_order(
load_body,
(
"vi->xattr_isize < sizeof(*ih)",
"vi->xattr_isize == sizeof(*ih)",
"error = EOPNOTSUPP",
"header_size = sizeof(*ih)",
),
"exact-header compatibility",
)
spec_path = dut / "tests/pre15/fixtures/B17-xattr-spec.json"
spec = json.loads(spec_path.read_text(encoding="ascii"))
cases = spec.get("cases", [])
legal = sum(item.get("class") == "legal" for item in cases)
damaged = sum(item.get("class") == "damaged" for item in cases)
if len(cases) != 25 or legal != 10 or damaged != 15:
raise SystemExit("B17 xattr fixture order/cardinality changed")
seed_path = dut / "tests/pre15/fixtures" / spec["seed"]["path"]
seed_hash = hashlib.sha256(seed_path.read_bytes()).hexdigest()
if seed_hash != spec["seed"]["sha256"]:
raise SystemExit("B17 xattr seed no longer matches its frozen specification")
result = {
"status": "PASS",
"baseline": baseline,
"source_sha256": hashlib.sha256(current.encode("utf-8")).hexdigest(),
"definition_count": len(current_names),
"direct_call_edges": len(current_calls),
"direct_call_sites": sum(current_calls.values()),
"static_visibility_unchanged": True,
"function_text_unchanged": True,
"linux_core_order": list(linux_aligned_core),
"freebsd_contracts": [
"positive errno",
"partial uiomove",
"extattr_check_cred",
"USER and SYSTEM namespaces",
"exact-header EOPNOTSUPP",
"uncached xattr lookup",
],
"b17_fixture_cases": len(cases),
"b17_fixture_legal": legal,
"b17_fixture_damaged": damaged,
"b17_seed_sha256": seed_hash,
}
output.write_text(
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="ascii"
)
print(
"B18 xattr order: exact moves, "
f"{len(current_names)} definitions, "
f"{sum(current_calls.values())} direct calls"
)
print(f"B18 B17 fixture contract: {len(cases)} cases, seed {seed_hash}")
PY
then
:
else
pre15_dut_fail 'B18 xattr ordering or FreeBSD contract equivalence failed'
fi
printf '%s\n' 'B18 xattr order: PASS'
+217
View File
@@ -0,0 +1,217 @@
#!/bin/sh
set -eu
: "$PRE15_DUT"
: "$PRE15_ROOT"
: "$PRE15_CASE_TMP"
: "$PRE15_RUN_DIR"
: "$PRE15_LIB_DIR"
. "$PRE15_LIB_DIR/runner.sh"
test "$PRE15_MODE" = qemu || pre15_infra_blocked \
'B19a focused case requires exact-ABI QEMU mode'
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
generator=$fixture_dir/B19a-xattr-generate.py
spec=$fixture_dir/B19a-xattr-spec.json
probe=$fixture_dir/B19a-xattr-probe.c
kld_builder=$fixture_dir/B28-build-kld.sh
artifacts=$PRE15_RUN_DIR/artifacts
first=$PRE15_CASE_TMP/fixtures-first
second=$PRE15_CASE_TMP/fixtures-second
module=$PRE15_CASE_TMP/B19a-erofs.ko
for tool in cc cmp diff file mkfs.erofs nm python3 scp sha256sum tar timeout; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B19a QEMU tool: $tool"
done
for value in PRE15_QEMU_CONTROL_PATH PRE15_QEMU_SSH_KEY \
PRE15_QEMU_SSH_PORT PRE15_QEMU_SSH_USER; do
test -n "$(eval "printf '%s' \"\$$value\"")" || \
pre15_runner_fail "$value is required"
done
for fixture in "$generator" "$spec" "$probe" "$kld_builder"; do
pre15_record_fixture "b19a-$(basename "$fixture")" "$fixture"
done
for source in erofs_vnops.c internal.h xattr.c; do
pre15_record_fixture "b19a-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
if ! timeout -k 5 180 python3 -B "$generator" --spec "$spec" \
--output "$first" --work "$PRE15_CASE_TMP/generate-first" \
>"$artifacts/generate-first.json" 2>"$artifacts/generate-first.stderr"; then
pre15_runner_fail 'B19a first fixture generation failed'
fi
if ! timeout -k 5 180 python3 -B "$generator" --spec "$spec" \
--output "$second" --work "$PRE15_CASE_TMP/generate-second" \
>"$artifacts/generate-second.json" 2>"$artifacts/generate-second.stderr"; then
pre15_runner_fail 'B19a second fixture generation failed'
fi
if ! diff -qr "$first" "$second" >"$artifacts/fixture-repeat.diff"; then
pre15_runner_fail 'B19a focused fixtures are not byte reproducible'
fi
pre15_record_fixture b19a-valid-image "$first/valid.erofs"
tar -C "$first" -czf "$PRE15_CASE_TMP/B19a-fixtures.tar.gz" .
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
"$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" 0 \
>"$artifacts/kld-build.stdout" 2>"$artifacts/kld-build.stderr"; then
pre15_dut_fail 'B19a exact-ABI KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/kld-file.txt"
sha256sum "$module" >"$artifacts/kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/kld-nm-u.txt"
pre15_scp()
{
timeout -k 5 60 scp -O -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
}
if ! pre15_scp "$PRE15_CASE_TMP/B19a-fixtures.tar.gz" \
/root/B19a-fixtures.tar.gz || ! pre15_scp "$probe" /root/B19a-xattr-probe.c ||
! pre15_scp "$module" /root/B19a-erofs.ko; then
pre15_infra_blocked 'could not transfer B19a module, probe, or fixtures'
fi
pre15_guest_ssh_bounded \
'rm -rf /root/pre15-b19a-fixtures && mkdir /root/pre15-b19a-fixtures && tar -xzf /root/B19a-fixtures.tar.gz -C /root/pre15-b19a-fixtures' ||
pre15_infra_blocked 'could not prepare B19a guest fixtures'
pre15_guest_ssh_bounded cc -std=c11 -Wall -Wextra -Werror \
-o /root/B19a-xattr-probe /root/B19a-xattr-probe.c ||
pre15_infra_blocked 'could not compile the B19a guest probe'
if pre15_guest_ssh_bounded kldstat -n B19a-erofs >/dev/null 2>&1; then
pre15_infra_blocked 'guest already has an EROFS KLD loaded'
fi
pre15_guest_ssh_bounded dmesg >"$artifacts/dmesg-before-load.txt"
if ! pre15_guest_ssh_bounded kldload /root/B19a-erofs.ko \
>"$artifacts/kldload.stdout" 2>"$artifacts/kldload.stderr"; then
pre15_dut_fail 'B19a exact-ABI KLD load failed'
fi
pre15_own_guest_kld B19a-erofs 'B19a exact-source KLD'
pre15_guest_ssh_bounded kldstat >"$artifacts/kldstat-after-load.txt"
b19a_unown()
{
tmp=$PRE15_CASE_TMP/ownership.$$
awk -F ' ' -v kind="$1" -v value="$2" \
'!($1 == kind && $2 == value)' "$PRE15_OWNERSHIP_FILE" >"$tmp"
mv "$tmp" "$PRE15_OWNERSHIP_FILE"
}
b19a_mount()
{
image=$1
mountpoint=$2
label=$3
md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
-f "/root/pre15-b19a-fixtures/$image") ||
pre15_dut_fail "$label provider attach failed"
case "$md" in
md[0-9]*) ;;
*) pre15_runner_fail "unexpected B19a md unit: $md" ;;
esac
pre15_own_guest_md "$md" "$label provider"
pre15_guest_ssh_bounded mkdir -p "$mountpoint"
if ! pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$md" \
"$mountpoint"; then
pre15_dut_fail "$label mount failed"
fi
pre15_own_guest_mount "$mountpoint" "$label mount"
printf '%s\n' "$md"
}
b19a_detach()
{
mountpoint=$1
md=$2
pre15_guest_ssh_bounded umount "$mountpoint" ||
pre15_dut_fail "B19a unmount failed: $mountpoint"
b19a_unown guest-mount "$mountpoint"
pre15_guest_ssh_bounded mdconfig -d -u "$md" ||
pre15_dut_fail "B19a md detach failed: $md"
b19a_unown guest-md "$md"
}
valid_md=$(b19a_mount valid.erofs /mnt/pre15-b19a-valid 'B19a valid')
valid=/mnt/pre15-b19a-valid/target.bin
pre15_guest_ssh_bounded /root/B19a-xattr-probe valid "$valid" \
>"$artifacts/valid-first.txt"
pre15_guest_ssh_bounded /root/B19a-xattr-probe concurrent "$valid" 16 10 \
>"$artifacts/valid-concurrent.txt"
pre15_guest_ssh_bounded /root/B19a-xattr-probe valid "$valid" \
>"$artifacts/valid-repeat.txt"
b19a_detach /mnt/pre15-b19a-valid "$valid_md"
for image in corrupt-shared-count.erofs corrupt-shared-id.erofs corrupt-inline-name.erofs; do
label=$(basename "$image" .erofs)
mountpoint=/mnt/pre15-b19a-$label
md=$(b19a_mount "$image" "$mountpoint" "B19a $label")
if ! pre15_guest_ssh_bounded /root/B19a-xattr-probe corrupt \
"$mountpoint/target.bin" >"$artifacts/$label.txt" 2>&1; then
pre15_dut_fail "$label did not return EINTEGRITY"
fi
pre15_guest_ssh_bounded umount "$mountpoint" ||
pre15_dut_fail "$label unmount failed"
b19a_unown guest-mount "$mountpoint"
pre15_guest_ssh_bounded mdconfig -d -u "$md" ||
pre15_dut_fail "$label md detach failed"
b19a_unown guest-md "$md"
done
normal_md=$(b19a_mount valid.erofs /mnt/pre15-b19a-normal \
'B19a normal-unmount')
normal_pid=$(pre15_guest_ssh_bounded \
'cd /mnt/pre15-b19a-normal && sleep 60 >/tmp/B19a-normal.sleep 2>&1 & echo $!')
if pre15_guest_ssh_bounded umount /mnt/pre15-b19a-normal \
>"$artifacts/normal-unmount.stdout" 2>"$artifacts/normal-unmount.stderr"; then
pre15_dut_fail 'normal unmount unexpectedly ignored a held vnode'
fi
pre15_guest_ssh_bounded kill "$normal_pid" || true
pre15_guest_ssh_bounded umount /mnt/pre15-b19a-normal ||
pre15_dut_fail 'normal unmount failed after releasing held vnode'
b19a_unown guest-mount /mnt/pre15-b19a-normal
pre15_guest_ssh_bounded mdconfig -d -u "$normal_md" ||
pre15_dut_fail 'normal-unmount md detach failed'
b19a_unown guest-md "$normal_md"
forced_md=$(b19a_mount valid.erofs /mnt/pre15-b19a-forced \
'B19a forced-unmount')
forced_pid=$(pre15_guest_ssh_bounded \
'cd /mnt/pre15-b19a-forced && sleep 60 >/tmp/B19a-forced.sleep 2>&1 & echo $!')
pre15_guest_ssh_bounded umount -f /mnt/pre15-b19a-forced ||
pre15_dut_fail 'forced unmount failed'
b19a_unown guest-mount /mnt/pre15-b19a-forced
pre15_guest_ssh_bounded kill "$forced_pid" || true
pre15_guest_ssh_bounded mdconfig -d -u "$forced_md" ||
pre15_dut_fail 'forced-unmount md detach failed'
b19a_unown guest-md "$forced_md"
pre15_guest_ssh_bounded dmesg >"$artifacts/dmesg-after.txt"
diff -u "$artifacts/dmesg-before-load.txt" "$artifacts/dmesg-after.txt" \
>"$artifacts/dmesg.diff" || true
sed -n '/^+++ /d; /^+/s/^+/ /p' "$artifacts/dmesg.diff" \
>"$artifacts/dmesg-added.txt"
if grep -Eqi 'panic:|fatal trap|lock order reversal|witness.*warning|use-after-free|pager fault|undefined symbol|linker.*error' \
"$artifacts/dmesg-added.txt"; then
pre15_dut_fail 'B19a QEMU produced a kernel diagnostic'
fi
pre15_guest_ssh_bounded kldunload B19a-erofs ||
pre15_dut_fail 'B19a KLD unload failed'
b19a_unown guest-kld B19a-erofs
if pre15_guest_ssh_bounded kldstat -n B19a-erofs >/dev/null 2>&1; then
pre15_dut_fail 'B19a KLD remained loaded after cleanup'
fi
pre15_guest_ssh_bounded mount >"$artifacts/mount-after.txt"
pre15_guest_ssh_bounded mdconfig -l >"$artifacts/md-after.txt"
if grep -q 'pre15-b19a-' "$artifacts/mount-after.txt" ||
grep -q 'B19a' "$artifacts/md-after.txt"; then
pre15_dut_fail 'B19a guest mount or md resource remained after cleanup'
fi
pre15_target_reached
printf '%s\n' 'B19a exact-ABI xattr cache lifecycle PASS'
+220
View File
@@ -0,0 +1,220 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
spec=$fixture_dir/B19b-xattr-spec.json
generator=$fixture_dir/B19b-xattr-generate.py
oracle=$fixture_dir/B19b-xattr-oracle.py
probe=$fixture_dir/B19b-xattr-probe.c
kld_builder=$fixture_dir/B19b-build-kld.sh
gate_script=$PRE15_DUT/tests/pre15/gates/P15-021.sh
gate_input=$PRE15_DUT/tests/pre15/gates/P15-021-input.json
gate_commit=675ed9b650b3bc157b38bcc165a0cb215a2aa989
gate_base=666e52f710363df07f7c93919eb835d41092d011
artifacts=$PRE15_RUN_DIR/artifacts
first=$PRE15_CASE_TMP/first
second=$PRE15_CASE_TMP/second
gate_clone=$PRE15_CASE_TMP/gate-clone
gate_output=$PRE15_CASE_TMP/gate-output
for tool in cmp fsck.erofs git mkfs.erofs python3 sha256sum timeout; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B19b host tool: $tool"
done
pre15_record_fixture b19b-spec "$spec"
pre15_record_fixture b19b-generator "$generator"
pre15_record_fixture b19b-oracle "$oracle"
pre15_record_fixture b19b-probe "$probe"
pre15_record_fixture b19b-kld-builder "$kld_builder"
pre15_record_fixture b19b-case "$PRE15_DUT/tests/pre15/cases/B19b-xattr-bloom.sh"
pre15_record_fixture b19b-gate "$gate_script"
pre15_record_fixture b19b-gate-input "$gate_input"
pre15_record_fixture b19b-erofs-fs "$PRE15_DUT/src/erofs_fs.h"
pre15_record_fixture b19b-internal "$PRE15_DUT/src/internal.h"
pre15_record_fixture b19b-xattr "$PRE15_DUT/src/xattr.c"
pre15_record_fixture b19b-linux-erofs-fs "$PRE15_ROOT/src-linux/erofs_fs.h"
pre15_record_fixture b19b-linux-xattr "$PRE15_ROOT/src-linux/xattr.c"
mkdir -p "$artifacts"
if ! git clone -q --shared --no-checkout "$PRE15_ROOT" "$gate_clone" \
>"$artifacts/gate-clone.stdout" 2>"$artifacts/gate-clone.stderr" || \
! git -C "$gate_clone" checkout -q --detach "$gate_commit" \
>"$artifacts/gate-checkout.stdout" 2>"$artifacts/gate-checkout.stderr"; then
pre15_runner_fail 'could not materialize the committed P15-021 gate'
fi
frozen_gate=$gate_clone/repo-pre-15/tests/pre15/gates/P15-021.sh
if ! timeout -k 10 240 "$frozen_gate" --base "$gate_base" \
--output "$gate_output" >"$artifacts/gate.stdout" \
2>"$artifacts/gate.stderr"; then
pre15_runner_fail 'committed P15-021 gate did not replay GO'
fi
if ! python3 -B "$generator" generate --spec "$spec" --output "$first" \
--work "$PRE15_CASE_TMP/first-work" >"$artifacts/generate-first.stdout" \
2>"$artifacts/generate-first.stderr"; then
pre15_runner_fail 'B19b first real-fixture generation failed'
fi
if ! python3 -B "$generator" generate --spec "$spec" --output "$second" \
--work "$PRE15_CASE_TMP/second-work" >"$artifacts/generate-second.stdout" \
2>"$artifacts/generate-second.stderr"; then
pre15_runner_fail 'B19b repeat real-fixture generation failed'
fi
if ! cmp "$first/SHA256SUMS" "$second/SHA256SUMS" || \
! cmp "$first/manifest.json" "$second/manifest.json"; then
pre15_runner_fail 'B19b fixture generation is not byte reproducible'
fi
for image in valid.erofs unknown-filter.erofs feature-off.erofs \
corrupt-shared-count.erofs corrupt-shared-id.erofs; do
if ! cmp "$first/$image" "$second/$image"; then
pre15_runner_fail "B19b repeat image differs: $image"
fi
pre15_record_fixture "b19b-$image" "$first/$image"
done
if ! python3 -B "$oracle" --spec "$spec" --fixtures "$first" \
--root "$PRE15_ROOT" --dut "$PRE15_DUT" \
--report "$artifacts/B19b-oracle.json" \
>"$artifacts/oracle.stdout" 2>"$artifacts/oracle.stderr"; then
pre15_dut_fail 'B19b fixture/source/Linux oracle failed'
fi
cp "$gate_output/result.json" "$artifacts/P15-021-gate-result.json"
cp "$gate_output/million.json" "$artifacts/P15-021-million.json"
cp "$gate_output/benchmark.json" "$artifacts/P15-021-benchmark.json"
cp "$first/SHA256SUMS" "$artifacts/B19b-SHA256SUMS"
pre15_record_fixture b19b-generated-sums "$artifacts/B19b-SHA256SUMS"
pre15_target_reached
if test "${PRE15_MODE:-host}" != qemu; then
printf '%s\n' \
'TC169 host real fixture/hash/filter/integrity/concurrency oracle PASS' \
'QEMU runtime NOT_RUN in host mode'
exit 0
fi
for tool in awk clang file nm scp tar; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B19b QEMU tool: $tool"
done
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
fixture_archive=$PRE15_CASE_TMP/B19b-fixtures.tar.gz
module=$PRE15_CASE_TMP/B19b-erofs.ko
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
"$PRE15_CASE_TMP/kld-work" >"$artifacts/B19b-kld-build.stdout" \
2>"$artifacts/B19b-kld-build.stderr"; then
pre15_dut_fail 'B19b cross-target KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/B19b-kld-file.txt"
sha256sum "$module" >"$artifacts/B19b-kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/B19b-kld-nm-u.txt"
tar -C "$first" -czf "$fixture_archive" .
pre15_scp()
{
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
}
if ! pre15_scp "$fixture_archive" /root/B19b-fixtures.tar.gz || \
! pre15_scp "$probe" /root/B19b-xattr-probe.c || \
! pre15_scp "$module" /root/B19b-erofs.ko; then
pre15_infra_blocked 'could not transfer B19b module, probe, or fixtures'
fi
if ! pre15_guest_ssh_bounded \
'rm -rf /root/pre15-b19b-fixtures && mkdir /root/pre15-b19b-fixtures && tar -xzf /root/B19b-fixtures.tar.gz -C /root/pre15-b19b-fixtures'; then
pre15_infra_blocked 'could not prepare B19b guest fixtures'
fi
if pre15_guest_ssh_bounded kldstat -n erofs >/dev/null 2>&1; then
pre15_infra_blocked 'FreeBSD guest already has an EROFS module loaded'
fi
if ! pre15_guest_ssh_bounded kldload /root/B19b-erofs.ko \
>"$artifacts/B19b-kldload.stdout" 2>"$artifacts/B19b-kldload.stderr"; then
if grep -q 'module already loaded or in kernel' "$artifacts/B19b-kldload.stderr"; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
pre15_dut_fail 'B19b exact-source KLD failed to load'
fi
pre15_own_guest_kld erofs 'B19b exact-source KLD'
if ! pre15_guest_ssh_bounded cc -std=c11 -Wall -Wextra -Werror \
-o /root/B19b-xattr-probe /root/B19b-xattr-probe.c; then
pre15_infra_blocked 'could not compile the B19b guest probe'
fi
pre15_mount_image()
{
pre15_image=$1
pre15_mountpoint=$2
pre15_label=$3
pre15_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
-f "/root/pre15-b19b-fixtures/$pre15_image") || \
pre15_dut_fail "could not attach B19b provider: $pre15_image"
case "$pre15_md" in
md[0-9]*) ;;
*) pre15_runner_fail "unexpected mdconfig output: $pre15_md" ;;
esac
pre15_own_guest_md "$pre15_md" "$pre15_label provider"
pre15_guest_ssh_bounded mkdir -p "$pre15_mountpoint"
if ! pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$pre15_md" \
"$pre15_mountpoint"; then
pre15_dut_fail "$pre15_label mount failed"
fi
pre15_own_guest_mount "$pre15_mountpoint" "$pre15_label mount"
}
pre15_mount_image valid.erofs /mnt/pre15-b19b-valid 'B19b valid'
valid=/mnt/pre15-b19b-valid/target.bin
pre15_guest_ssh_bounded /root/B19b-xattr-probe hit "$valid" attr00 0
pre15_guest_ssh_bounded /root/B19b-xattr-probe errno "$valid" user \
absent-00001 87
pre15_guest_ssh_bounded /root/B19b-xattr-probe errno "$valid" user \
absent-00000 87
pre15_guest_ssh_bounded /root/B19b-xattr-probe list "$valid"
pre15_guest_ssh_bounded /root/B19b-xattr-probe concurrent "$valid" attr00 \
absent-00001 absent-00000 64 10
pre15_mount_image unknown-filter.erofs /mnt/pre15-b19b-unknown \
'B19b unknown filter'
unknown=/mnt/pre15-b19b-unknown/target.bin
pre15_guest_ssh_bounded /root/B19b-xattr-probe hit "$unknown" attr00 0
pre15_guest_ssh_bounded /root/B19b-xattr-probe errno "$unknown" user \
absent-00001 87
pre15_mount_image feature-off.erofs /mnt/pre15-b19b-feature-off \
'B19b feature off'
feature_off=/mnt/pre15-b19b-feature-off/target.bin
pre15_guest_ssh_bounded /root/B19b-xattr-probe hit "$feature_off" attr00 0
pre15_guest_ssh_bounded /root/B19b-xattr-probe errno "$feature_off" user \
absent-00001 87
pre15_mount_image corrupt-shared-count.erofs /mnt/pre15-b19b-bad-count \
'B19b corrupt shared count'
pre15_guest_ssh_bounded /root/B19b-xattr-probe errno \
/mnt/pre15-b19b-bad-count/target.bin user absent-00001 97
pre15_mount_image corrupt-shared-id.erofs /mnt/pre15-b19b-bad-id \
'B19b corrupt shared ID'
pre15_guest_ssh_bounded /root/B19b-xattr-probe errno \
/mnt/pre15-b19b-bad-id/target.bin user attr00 97
pre15_guest_ssh_bounded dmesg >"$artifacts/B19b-dmesg.txt"
if grep -Eq 'panic:|Fatal trap|lock order reversal|KDB: stack backtrace' \
"$artifacts/B19b-dmesg.txt"; then
pre15_dut_fail 'B19b runtime produced a kernel diagnostic'
fi
printf '%s\n' 'TC169 QEMU hit/miss/collision/fallback/integrity/concurrency PASS'
+540
View File
@@ -0,0 +1,540 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=e769e4ae32d4967c1f69067dc92628b60b62b648
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
spec=$fixture_dir/B21-super-spec.json
generator=$fixture_dir/B21-super-generate.py
oracle=$fixture_dir/B21-super-oracle.py
qemu_probe=$fixture_dir/B21-qemu-probe.c
kld_builder=$fixture_dir/B28-build-kld.sh
artifacts=$PRE15_RUN_DIR/artifacts
first=$PRE15_CASE_TMP/first
second=$PRE15_CASE_TMP/second
for tool in cmp fsck.erofs git mkfs.erofs python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B21 host tool: $tool"
done
pre15_record_fixture b21-spec "$spec"
pre15_record_fixture b21-generator "$generator"
pre15_record_fixture b21-oracle "$oracle"
pre15_record_fixture b21-qemu-probe "$qemu_probe"
pre15_record_fixture b21-kld-builder "$kld_builder"
pre15_record_fixture b21-case "$PRE15_DUT/tests/pre15/cases/B21-super.sh"
pre15_record_fixture b21-super "$PRE15_DUT/src/super.c"
pre15_record_fixture b21-internal "$PRE15_DUT/src/internal.h"
pre15_record_fixture b21-xattr "$PRE15_DUT/src/xattr.c"
mkdir -p "$artifacts"
if python3 -B "$generator" --spec "$spec" --output "$first" \
--work "$PRE15_CASE_TMP/first-work" \
>"$artifacts/generate-first.json" \
2>"$artifacts/generate-first.stderr"; then
:
else
pre15_runner_fail 'B21 first fixture generation failed'
fi
if python3 -B "$generator" --spec "$spec" --output "$second" \
--work "$PRE15_CASE_TMP/second-work" \
>"$artifacts/generate-second.json" \
2>"$artifacts/generate-second.stderr"; then
:
else
pre15_runner_fail 'B21 second fixture generation failed'
fi
if cmp "$first/fixture-index.json" "$second/fixture-index.json" && \
find "$first" -type f ! -name fixture-index.json -printf '%f\n' | sort | \
while IFS= read -r name; do cmp "$first/$name" "$second/$name" || exit 1; done
then
:
else
pre15_runner_fail 'B21 fixture generation is not byte reproducible'
fi
if python3 -B "$oracle" --fixtures "$first" \
--report "$artifacts/oracle-first.json" \
>"$artifacts/oracle-first.stdout" 2>"$artifacts/oracle-first.stderr" && \
python3 -B "$oracle" --fixtures "$second" \
--report "$artifacts/oracle-second.json" \
>"$artifacts/oracle-second.stdout" 2>"$artifacts/oracle-second.stderr" && \
cmp "$artifacts/oracle-first.json" "$artifacts/oracle-second.json"
then
:
else
pre15_runner_fail 'B21 independent oracle replay failed'
fi
if fsck.erofs -d0 "$first/legal-control.erofs" \
>"$artifacts/legal-fsck.stdout" 2>"$artifacts/legal-fsck.stderr"; then
:
else
pre15_runner_fail 'B21 legal control is not accepted by fsck.erofs'
fi
cp "$first/fixture-index.json" "$artifacts/B21-fixture-index.json"
pre15_record_fixture b21-generated-index "$artifacts/B21-fixture-index.json"
pre15_target_reached
if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
:
else
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \
"$artifacts/B21-source-check.json" <<'PY'
from __future__ import annotations
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
baseline = sys.argv[3]
output = Path(sys.argv[4])
src = dut / "src"
def committed(name: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/src/{name}"],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
raise SystemExit(f"cannot read B21 baseline {name}: {completed.stderr}")
return completed.stdout
def replace_once(source: str, old: str, new: str, label: str) -> str:
count = source.count(old)
if count != 1:
raise SystemExit(f"{label}: expected one transform source, found {count}")
return source.replace(old, new, 1)
def extract_function(source: str, name: str) -> str:
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if not match:
raise SystemExit(f"missing function: {name}")
name_line = source.rfind("\n", 0, match.start()) + 1
start = source.rfind("\n", 0, name_line - 1) + 1
brace = source.find("{", match.end())
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
raise SystemExit(f"unterminated function: {name}")
def require_order(source: str, markers: list[str], label: str) -> None:
position = -1
for marker in markers:
position = source.find(marker, position + 1)
if position < 0:
raise SystemExit(f"{label} is missing ordered marker: {marker}")
current = {
name: (src / name).read_text(encoding="utf-8")
for name in ("internal.h", "super.c", "xattr.c")
}
base = {name: committed(name) for name in current}
if current["internal.h"] != base["internal.h"]:
raise SystemExit("B21 changed internal.h despite all required helpers existing")
for helper in (
"EROFS_FEATURE_FUNCS(fragments, incompat, INCOMPAT_FRAGMENTS)",
"EROFS_FEATURE_FUNCS(sb_chksum, compat, COMPAT_SB_CHKSUM)",
"EROFS_FEATURE_FUNCS(plain_xattr_pfx, compat, COMPAT_PLAIN_XATTR_PFX)",
):
if current["internal.h"].count(helper) != 1:
raise SystemExit(f"B21 feature helper is missing or duplicated: {helper}")
expected_super = replace_once(
base["super.c"],
"\tif ((le32toh(dsb->feature_compat) & EROFS_FEATURE_COMPAT_SB_CHKSUM) == 0)\n",
"\tif (!erofs_sb_has_sb_chksum(sbi))\n",
"checksum helper",
)
expected_super = replace_once(
expected_super,
"\tif ((sbi->feature_incompat & EROFS_FEATURE_INCOMPAT_FRAGMENTS) != 0 &&\n"
"\t sbi->packed_nid > 0) {\n",
"\tif (erofs_sb_has_fragments(sbi) && sbi->packed_nid > 0) {\n",
"fragments helper",
)
old_read = extract_function(expected_super, "erofs_read_superblock")
new_read = extract_function(current["super.c"], "erofs_read_superblock")
expected_read = replace_once(
old_read,
"\tif (dsb->dirblkbits != 0)\n"
"\t\treturn (EOPNOTSUPP);\n"
"\tsbi->feature_compat = le32toh(dsb->feature_compat);\n",
"\tsbi->blkszbits = dsb->blkszbits;\n"
"\tsbi->block_size = 1u << sbi->blkszbits;\n"
"\tsbi->feature_compat = le32toh(dsb->feature_compat);\n"
"\terror = erofs_superblock_csum_verify(sbi, dsb);\n"
"\tif (error != 0)\n"
"\t\treturn (error);\n\n"
"\tif (dsb->dirblkbits != 0)\n"
"\t\treturn (EOPNOTSUPP);\n",
"checksum trust order",
)
dead_exception = """\t/*
\t * Narrowly allow one extra combination: long xattr prefixes enabled
\t * with non-plain prefix table stored in a packed inode, which adds
\t * the FRAGMENTS (0x20) incompat bit. This is NOT a declaration of
\t * general fragments support; per-inode data layout is still gated
\t * by plain/inline checks in erofs_read_inode().
\t */
\tif (unsupported != 0) {
\t\tif (unsupported != EROFS_FEATURE_INCOMPAT_FRAGMENTS ||
\t\t (sbi->feature_incompat &
\t\t\tEROFS_FEATURE_INCOMPAT_XATTR_PREFIXES) == 0 ||
\t\t (sbi->feature_compat &
\t\t\tEROFS_FEATURE_COMPAT_PLAIN_XATTR_PFX) != 0 ||
\t\t sbi->packed_nid == 0)
\t\t\treturn (EOPNOTSUPP);
\t}
\tsbi->blkszbits = dsb->blkszbits;
\tsbi->block_size = 1u << sbi->blkszbits;
"""
expected_read = replace_once(
expected_read,
dead_exception,
"\tif (unsupported != 0)\n\t\treturn (EOPNOTSUPP);\n",
"dead fragments exception",
)
old_checksum_site = (
"\terror = erofs_superblock_csum_verify(sbi, dsb);\n"
"\tif (error != 0)\n"
"\t\treturn (error);\n"
)
if expected_read.count(old_checksum_site) != 2:
raise SystemExit("checksum trust-order transform did not produce two sites")
old_checksum_offset = expected_read.rfind(old_checksum_site)
expected_read = (
expected_read[:old_checksum_offset]
+ expected_read[old_checksum_offset + len(old_checksum_site) :]
)
if new_read != expected_read:
raise SystemExit("erofs_read_superblock differs from exact B21 transforms")
expected_super = expected_super.replace(old_read, expected_read, 1)
if current["super.c"] != expected_super:
raise SystemExit("super.c changed outside exact B21 transforms")
expected_xattr = replace_once(
base["xattr.c"],
"\tif ((sbi->feature_incompat & EROFS_FEATURE_INCOMPAT_XATTR_PREFIXES) ==\n"
"\t\t0 ||\n"
"\t sbi->xattr_prefix_count == 0)\n",
"\tif (!erofs_sb_has_xattr_prefixes(sbi) || sbi->xattr_prefix_count == 0)\n",
"xattr-prefix helper",
)
expected_xattr = replace_once(
expected_xattr,
"\tif ((sbi->feature_compat & EROFS_FEATURE_COMPAT_PLAIN_XATTR_PFX) == 0) {\n",
"\tif (!erofs_sb_has_plain_xattr_pfx(sbi)) {\n",
"plain-prefix helper",
)
if current["xattr.c"] != expected_xattr:
raise SystemExit("xattr.c changed outside exact B21 helper conversions")
read_super = new_read
checksum_call = read_super.index("erofs_superblock_csum_verify(sbi, dsb)")
before_checksum = read_super[:checksum_call]
for protected in (
"dirblkbits",
"feature_incompat",
"packed_nid",
"extra_devices",
"sb_extslots",
"meta_blkaddr",
"xattr_blkaddr",
"xattr_prefix_start",
"xattr_prefix_count",
"blocks_root",
):
if protected in before_checksum:
raise SystemExit(f"protected field used before checksum: {protected}")
require_order(
read_super,
[
"dsb->magic",
"dsb->blkszbits < 9",
"sbi->feature_compat = le32toh(dsb->feature_compat)",
"erofs_superblock_csum_verify(sbi, dsb)",
"dsb->dirblkbits",
"sbi->feature_incompat = le32toh(dsb->feature_incompat)",
"unsupported = sbi->feature_incompat",
"sbi->sb_size = 128 + dsb->sb_extslots",
"sbi->xattr_prefix_start = le32toh(dsb->xattr_prefix_start)",
"erofs_validate_device_size",
"erofs_load_generation_seed",
"z_erofs_parse_cfgs",
],
"superblock trust order",
)
mountfs = extract_function(current["super.c"], "erofs_mountfs")
require_order(
mountfs,
[
"erofs_read_superblock",
"erofs_scan_devices",
"erofs_init_packed_inode",
"erofs_init_metabox_inode",
"erofs_read_inode(sbi, sbi->root_nid",
"erofs_xattr_prefixes_init",
"mp->mnt_data = sbi",
],
"FreeBSD mount publication",
)
if "fail:\n\terofs_sb_free(sbi);\n\treturn (error);" not in mountfs:
raise SystemExit("mount failure no longer funnels through erofs_sb_free")
sb_free = extract_function(current["super.c"], "erofs_sb_free")
require_order(
sb_free,
[
"z_erofs_extent_cache_fini",
"erofs_xattr_prefixes_cleanup",
"erofs_drop_internal_inodes",
"erofs_free_dev_context",
"erofs_release_device_info(&sbi->dif0)",
"free(sbi, M_EROFS)",
],
"mount cleanup",
)
release = extract_function(current["super.c"], "erofs_release_device_info")
require_order(
release,
["g_topology_lock", "g_vfs_close", "g_topology_unlock", "vrele", "dev_rel"],
"GEOM release",
)
if any("return (-E" in current[name] for name in current):
raise SystemExit("negative errno entered the FreeBSD B21 write set")
result = {
"baseline": baseline,
"checksum_before_protected_fields": True,
"dead_fragments_exception_removed": True,
"feature_helpers": True,
"geom_release_preserved": True,
"internal_h_unchanged": True,
"mount_cleanup_preserved": True,
"mount_publication_preserved": True,
"positive_errno_preserved": True,
"status": "PASS",
}
output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="ascii")
print("B21 source: exact trust-order and feature-helper transforms present")
PY
then
:
else
pre15_dut_fail 'B21 source contract failed'
fi
fi
if python3 - "$artifacts/B21-fixture-index.json" \
"$artifacts/oracle-first.json" "$artifacts/B21-matrix.tsv" <<'PY'
import json
from pathlib import Path
import sys
index = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
oracle = json.loads(Path(sys.argv[2]).read_text(encoding="ascii"))
if index["case_count"] != 13 or index["legal_count"] != 1 or index["damaged_count"] != 12:
raise SystemExit("B21 fixture cardinality changed")
if oracle["status"] != "PASS" or oracle["passed_count"] != 13:
raise SystemExit("B21 oracle matrix is incomplete")
lines = ["id\tauthenticated\terrno\treject\tsha256"]
for fixture, result in zip(index["cases"], oracle["results"], strict=True):
lines.append(
f"{fixture['id']}\t{str(fixture['authenticated']).lower()}\t"
f"{result['actual_errno']}\t{result['actual_reject']}\t{fixture['sha256']}"
)
Path(sys.argv[3]).write_text("\n".join(lines) + "\n", encoding="ascii")
print(
f"B21 fixtures: {index['case_count']} "
f"({index['legal_count']} legal, {index['damaged_count']} damaged)"
)
PY
then
:
else
pre15_runner_fail 'B21 fixture matrix summary failed'
fi
if test "${PRE15_MODE:-host}" != qemu; then
printf '%s\n' \
'B21 super gate: host PASS' \
'TC172 QEMU runtime NOT_RUN in host mode'
exit 0
fi
for tool in awk clang file nm scp tar timeout; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B21 QEMU tool: $tool"
done
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
fixture_archive=$PRE15_CASE_TMP/B21-fixtures.tar.gz
module=$PRE15_CASE_TMP/B21-erofs-zstdio0.ko
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
"$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" 0 \
>"$artifacts/B21-kld-build.stdout" 2>"$artifacts/B21-kld-build.stderr"; then
pre15_dut_fail 'B21 cross-target zstdio0 KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/B21-kld-file.txt"
sha256sum "$module" >"$artifacts/B21-kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/B21-kld-nm-u.txt"
tar -C "$first" -czf "$fixture_archive" .
pre15_scp()
{
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
}
if ! pre15_scp "$fixture_archive" /root/B21-fixtures.tar.gz || \
! pre15_scp "$qemu_probe" /root/B21-qemu-probe.c || \
! pre15_scp "$module" /root/B21-erofs-zstdio0.ko; then
pre15_infra_blocked 'could not transfer B21 module, probe, or fixtures'
fi
if ! pre15_guest_ssh_bounded \
'rm -rf /root/B21-fixtures && mkdir /root/B21-fixtures && tar -xzf /root/B21-fixtures.tar.gz -C /root/B21-fixtures && cc -O2 -Wall -Wextra -Werror -std=c17 -o /root/B21-qemu-probe /root/B21-qemu-probe.c'; then
pre15_infra_blocked 'could not prepare B21 guest fixtures/probe'
fi
if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
if ! pre15_guest_ssh_bounded kldload /root/B21-erofs-zstdio0.ko \
>"$artifacts/B21-kldload.stdout" 2>"$artifacts/B21-kldload.stderr"; then
if grep -q 'module already loaded or in kernel' \
"$artifacts/B21-kldload.stderr"; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
pre15_dut_fail 'B21 exact-source zstdio0 KLD failed to load'
fi
pre15_own_guest_kld erofs 'B21 exact-source KLD'
pre15_guest_ssh_bounded dmesg >"$artifacts/B21-dmesg-before.txt"
pre15_guest_ssh_bounded mkdir -p /mnt/pre15-b21
b21_unown()
{
kind=$1
value=$2
tmp=$PRE15_CASE_TMP/ownership.$$
awk -F '\t' -v kind="$kind" -v value="$value" \
'!($1 == kind && $2 == value)' "$PRE15_OWNERSHIP_FILE" >"$tmp"
mv "$tmp" "$PRE15_OWNERSHIP_FILE"
}
printf 'id\texpected_errno\tactual_errno\treject\n' \
>"$artifacts/B21-qemu-matrix.tsv"
while IFS="$(printf '\t')" read -r id authenticated expected_errno reject fixture_hash; do
test "$id" != id || continue
b21_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
-f "/root/B21-fixtures/$id.erofs") || \
pre15_dut_fail "$id md attach failed"
case "$b21_md" in
md[0-9]*) ;;
*) pre15_runner_fail "unexpected B21 md unit: $b21_md" ;;
esac
pre15_own_guest_md "$b21_md" "$id md"
result=$(pre15_guest_ssh_bounded /root/B21-qemu-probe \
"/dev/$b21_md" /mnt/pre15-b21) || \
pre15_infra_blocked "$id guest mount probe failed"
actual_errno=$(printf '%s\n' "$result" | sed -n 's/.* errno=\([0-9][0-9]*\).*/\1/p')
test -n "$actual_errno" || pre15_runner_fail "$id mount probe did not report errno"
if test "$actual_errno" = 0; then
pre15_own_guest_mount /mnt/pre15-b21 "$id mount"
fi
printf '%s\t%s\t%s\t%s\n' "$id" "$expected_errno" \
"$actual_errno" "$reject" >>"$artifacts/B21-qemu-matrix.tsv"
if test "$actual_errno" != "$expected_errno"; then
pre15_dut_fail "$id expected errno $expected_errno, received $actual_errno"
fi
if test "$actual_errno" = 0; then
pre15_guest_ssh_bounded find /mnt/pre15-b21 -mindepth 1 -maxdepth 1 \
-print >"$artifacts/B21-legal-readdir.txt"
pre15_guest_ssh_bounded umount /mnt/pre15-b21 || \
pre15_dut_fail "$id unmount failed"
b21_unown guest-mount /mnt/pre15-b21
fi
pre15_guest_ssh_bounded mdconfig -d -u "$b21_md" || \
pre15_dut_fail "$id md detach failed"
b21_unown guest-md "$b21_md"
done <"$artifacts/B21-matrix.tsv"
pre15_guest_ssh_bounded dmesg >"$artifacts/B21-dmesg-after.txt"
diff -u "$artifacts/B21-dmesg-before.txt" "$artifacts/B21-dmesg-after.txt" \
>"$artifacts/B21-dmesg.diff" || true
sed -n '/^+++ /d; /^+/s/^+//p' "$artifacts/B21-dmesg.diff" \
>"$artifacts/B21-dmesg-added.txt"
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free' \
"$artifacts/B21-dmesg-added.txt"; then
pre15_dut_fail 'TC172 produced panic, WITNESS, or UAF evidence'
fi
printf '%s\n' \
'TC172-super-trust B21 QEMU PASS' \
'13/13 legal/damaged images returned exact mount errno with checksum trust order preserved'
+570
View File
@@ -0,0 +1,570 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=8a8761af91654a025fd991d14da906c0b612bd7d
fixture=$PRE15_DUT/tests/pre15/fixtures/B22-zmap-arithmetic.json
kld_builder=$PRE15_DUT/tests/pre15/fixtures/B22-build-kld.sh
zmap=$PRE15_DUT/src/zmap.c
linux_zmap=$PRE15_ROOT/src-linux/zmap.c
artifacts=$PRE15_RUN_DIR/artifacts
for tool in cc git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B22 host tool: $tool"
done
pre15_record_fixture b22-arithmetic "$fixture"
pre15_record_fixture b22-kld-builder "$kld_builder"
pre15_record_fixture b22-case "$PRE15_DUT/tests/pre15/cases/B22-zmap-arithmetic.sh"
pre15_record_fixture b22-zmap "$zmap"
pre15_record_fixture b22-linux-zmap "$linux_zmap"
mkdir -p "$artifacts"
pre15_target_reached
if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$fixture" \
"$artifacts" <<'PY'
from __future__ import annotations
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
baseline = sys.argv[3]
fixture_path = Path(sys.argv[4])
artifacts = Path(sys.argv[5])
src = dut / "src"
u32_max = (1 << 32) - 1
u64_max = (1 << 64) - 1
eintegrity = 97
def committed(path: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
raise SystemExit(f"cannot read B22 baseline {path}: {completed.stderr}")
return completed.stdout
def function(source: str, name: str) -> str:
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if match is None:
raise SystemExit(f"missing function: {name}")
name_line = source.rfind("\n", 0, match.start()) + 1
start = source.rfind("\n", 0, name_line - 1) + 1
brace = source.find("{", match.end())
if brace < 0:
raise SystemExit(f"missing function body: {name}")
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
raise SystemExit(f"unterminated function: {name}")
def add(left: int, right: int) -> tuple[str, int | None]:
value = left + right
if value > u64_max:
return ("EINTEGRITY", None)
return ("PASS", value)
def multiply(left: int, right: int) -> tuple[str, int | None]:
value = left * right
if value > u64_max:
return ("EINTEGRITY", None)
return ("PASS", value)
def model(case: dict[str, object]) -> tuple[str, int | None]:
operation = case["operation"]
if operation == "compact_pblk":
return add(int(case["base"]), int(case["nblk"]))
if operation == "index_base":
status, end = add(int(case["inode_off"]), int(case["inode_isize"]))
if status != "PASS":
return (status, None)
status, end = add(int(end), int(case.get("xattr_isize", 0)))
if status != "PASS":
return (status, None)
status, end = add(int(end), 7)
if status != "PASS":
return (status, None)
end = int(end) & ~7
return add(end, 8)
if operation == "index_advance":
status, delta = multiply(int(case["count"]), int(case["unit"]))
if status != "PASS":
return (status, None)
return add(int(case["position"]), int(delta))
if operation == "lcluster_count":
bits = int(case["lclusterbits"])
if bits >= 64:
return ("EINTEGRITY", None)
size = int(case["size"])
count = size >> bits
if size & ((1 << bits) - 1):
return add(count, 1)
return ("PASS", count)
if operation == "lcluster_pos":
bits = int(case["lclusterbits"])
if bits >= 64:
return ("EINTEGRITY", None)
status, base = multiply(int(case["lcn"]), 1 << bits)
if status != "PASS":
return (status, None)
return add(int(base), int(case["clusterofs"]))
if operation == "lcn_advance":
return add(int(case["lcn"]), int(case["delta"]))
if operation == "physical_end":
status, pend = add(int(case["pa"]), int(case["plen"]))
if status != "PASS":
return (status, None)
limit = (1 << 48) * int(case["block_size"])
if limit <= u64_max and int(pend) > limit:
return ("EINTEGRITY", None)
return ("PASS", None)
if operation == "post_eof":
la = int(case["la"])
size = int(case["size"])
if la < size:
return ("EINTEGRITY", None)
length = la - size + 1
return ("PASS", min(length, u64_max))
if operation == "fragment_offset":
low = int(case["low"])
high = int(case["high"])
if low > u32_max or high > u32_max:
return ("EINTEGRITY", None)
return ("PASS", low | (high << 32))
raise SystemExit(f"unknown B22 operation: {operation}")
fixture = json.loads(fixture_path.read_text(encoding="ascii"))
if fixture.get("schema") != 1 or fixture.get("batch") != "B22":
raise SystemExit("invalid B22 fixture identity")
if fixture.get("test") != "TC170-zmap-arithmetic":
raise SystemExit("invalid B22 test identity")
if fixture.get("candidates") != [
"P15-047",
"P15-059",
"P15-071",
"P15-079",
"P15-084",
]:
raise SystemExit("B22 candidate set changed")
if fixture.get("errno") != {
"corruption": eintegrity,
"provider_io": "unchanged",
"sign": "positive",
}:
raise SystemExit("B22 errno contract changed")
cases = fixture.get("cases")
if not isinstance(cases, list) or not cases:
raise SystemExit("B22 fixture has no cases")
names: set[str] = set()
markers: set[str] = set()
operations: set[str] = set()
decoded: list[dict[str, object]] = []
for case in cases:
if not isinstance(case, dict):
raise SystemExit("B22 fixture case is not an object")
name = str(case.get("name", ""))
marker = str(case.get("target_marker", ""))
if not name or name in names or not marker.startswith("TC170:"):
raise SystemExit(f"invalid B22 case identity: {name!r}")
names.add(name)
markers.add(marker)
operations.add(str(case.get("operation", "")))
status, value = model(case)
if status != case.get("status"):
raise SystemExit(f"B22 independent status mismatch: {name}")
if status == "PASS" and value is not None and value != case.get("expected"):
raise SystemExit(f"B22 independent value mismatch: {name}")
mutated = case.get("mutated_fields")
if status == "EINTEGRITY" and (
not isinstance(mutated, list) or len(mutated) != 1
):
raise SystemExit(f"B22 negative is not single-field: {name}")
if status == "PASS" and mutated != []:
raise SystemExit(f"B22 positive declares a mutation: {name}")
decoded.append({"name": name, "status": status, "value": value})
required_markers = {
"TC170:compact-pblk-32bit-crossing",
"TC170:index-position",
"TC170:delta-lcn",
"TC170:physical-end-48bit",
"TC170:post-eof",
"TC170:fragment-high-bits",
}
if markers != required_markers:
raise SystemExit("B22 target-marker set is incomplete")
if operations != {
"compact_pblk",
"fragment_offset",
"index_advance",
"index_base",
"lcluster_count",
"lcluster_pos",
"lcn_advance",
"physical_end",
"post_eof",
}:
raise SystemExit("B22 operation set is incomplete")
current = (src / "zmap.c").read_text(encoding="utf-8")
base = committed("src/zmap.c")
linux = (root / "src-linux/zmap.c").read_text(encoding="utf-8")
helpers = [
"z_erofs_index_base",
"z_erofs_index_advance",
"z_erofs_lcluster_count",
"z_erofs_lcluster_pos",
"z_erofs_lcn_advance",
"z_erofs_compact_pblk",
"z_erofs_fragment_offset",
"z_erofs_post_eof_len",
"z_erofs_physical_end",
]
for helper in helpers:
if helper in base or current.count(helper) < 2:
raise SystemExit(f"B22 helper is missing, duplicated, or pre-existing: {helper}")
old_freebsd = (
"m->pblk = le32dec(in + packsize - sizeof(uint32_t)) + nblk;",
"map->m_llen = map->m_la + 1 - vi->size;",
"vi->z_fragmentoff |= map->m_pa << 32;",
"lcn += m->delta[1];",
"pos += lcn << amortizedshift;",
"(pend >> sbi->blkszbits) >= (1ULL << 48)",
)
if not all(anchor in base for anchor in old_freebsd):
raise SystemExit("B22 baseline arithmetic anchors changed")
if any(anchor in current for anchor in old_freebsd):
raise SystemExit("B22 left an unchecked baseline arithmetic anchor")
linux_anchors = (
"m->pblk = le32_to_cpu(*(__le32 *)in) + nblk;",
"map->m_llen = map->m_la + 1 - inode->i_size;",
"vi->z_fragmentoff |= map->m_pa << 32;",
"lcn += m->delta[1];",
"(pend >> sbi->blkszbits) >= BIT_ULL(48)",
)
if not all(anchor in linux for anchor in linux_anchors):
raise SystemExit("Linux zmap semantic anchor changed")
if function(current, "z_erofs_read_index") != function(base, "z_erofs_read_index"):
raise SystemExit("B22 changed the FreeBSD metadata/provider reader")
if current.count("erofs_read_metadata") != base.count("erofs_read_metadata"):
raise SystemExit("B22 changed metadata I/O call count")
if current.count("erofs_put_metabuf") != base.count("erofs_put_metabuf"):
raise SystemExit("B22 changed metadata release call count")
for public in ("z_erofs_fill_inode", "z_erofs_map_blocks"):
old_signature = function(base, public).split("{", 1)[0]
new_signature = function(current, public).split("{", 1)[0]
if old_signature != new_signature:
raise SystemExit(f"B22 changed public zmap ABI: {public}")
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", current):
raise SystemExit("B22 introduced Linux negative errno")
legacy_mismatches: set[str] = set()
for case in cases:
operation = case["operation"]
status = case["status"]
expected = case.get("expected")
if operation == "compact_pblk" and status == "PASS":
old = (int(case["base"]) + int(case["nblk"])) & u32_max
if old != expected:
legacy_mismatches.add("compact-pblk")
elif operation == "index_advance" and status == "EINTEGRITY":
old = (int(case["position"]) + int(case["count"]) * int(case["unit"])) & u64_max
if old <= u64_max:
legacy_mismatches.add("index-position")
elif operation == "lcluster_pos" and status == "EINTEGRITY":
old = ((int(case["lcn"]) << int(case["lclusterbits"])) + int(case["clusterofs"])) & u64_max
if old <= u64_max:
legacy_mismatches.add("delta-lcn")
elif operation == "physical_end" and case["name"] == "physical-last-48bit-block":
pend = int(case["pa"]) + int(case["plen"])
if (pend >> 12) >= (1 << 48):
legacy_mismatches.add("physical-end")
elif operation == "post_eof" and case["name"] == "post-eof-saturates-empty-inode":
old = ((int(case["la"]) + 1) & u64_max) - int(case["size"])
if old != expected:
legacy_mismatches.add("post-eof")
elif operation == "fragment_offset" and status == "EINTEGRITY":
old = int(case["low"]) | ((int(case["high"]) << 32) & u64_max)
if old <= u64_max:
legacy_mismatches.add("fragment-high")
if legacy_mismatches != {
"compact-pblk",
"delta-lcn",
"fragment-high",
"index-position",
"physical-end",
"post-eof",
}:
raise SystemExit("B22 fixtures do not distinguish every legacy bug")
helper_source = "\n\n".join(function(current, name) for name in helpers)
c_lines = [
"#include <stdint.h>",
"#include <stdio.h>",
"#define EINTEGRITY 97",
"#define rounddown2(x, y) ((x) & ~((y) - 1))",
"typedef uint64_t erofs_off_t;",
"typedef uint64_t erofs_blk_t;",
"struct z_erofs_map_header { uint8_t bytes[8]; };",
"struct erofs_inode { erofs_off_t inode_off; unsigned int inode_isize; unsigned int xattr_isize; };",
helper_source,
"int main(void)",
"{",
"\tuint64_t value;",
"\tint error, failures = 0;",
]
def u64(value: object) -> str:
return f"UINT64_C({int(value)})"
for case in cases:
name = str(case["name"])
operation = case["operation"]
expected_error = 0 if case["status"] == "PASS" else eintegrity
c_lines.append(f"\t/* {name} */")
if operation == "compact_pblk":
call = f"z_erofs_compact_pblk(UINT32_C({int(case['base'])}), {int(case['nblk'])}U, &value)"
elif operation == "index_base":
c_lines.extend([
"\t{",
"\t\tstruct erofs_inode vi = {",
f"\t\t\t.inode_off = {u64(case['inode_off'])},",
f"\t\t\t.inode_isize = {int(case['inode_isize'])}U,",
f"\t\t\t.xattr_isize = {int(case.get('xattr_isize', 0))}U,",
"\t\t};",
])
call = "z_erofs_index_base(&vi, &value)"
elif operation == "index_advance":
c_lines.append(f"\tvalue = {u64(case['position'])};")
call = f"z_erofs_index_advance(&value, {u64(case['count'])}, {u64(case['unit'])})"
elif operation == "lcluster_count":
call = f"z_erofs_lcluster_count({u64(case['size'])}, {int(case['lclusterbits'])}U, &value)"
elif operation == "lcluster_pos":
call = f"z_erofs_lcluster_pos({u64(case['lcn'])}, {int(case['lclusterbits'])}U, {u64(case['clusterofs'])}, &value)"
elif operation == "lcn_advance":
c_lines.append(f"\tvalue = {u64(case['lcn'])};")
call = f"z_erofs_lcn_advance(&value, {u64(case['delta'])})"
elif operation == "physical_end":
call = f"z_erofs_physical_end({u64(case['pa'])}, {u64(case['plen'])}, {u64(case['block_size'])})"
elif operation == "post_eof":
call = f"z_erofs_post_eof_len({u64(case['la'])}, {u64(case['size'])}, &value)"
elif operation == "fragment_offset":
call = f"z_erofs_fragment_offset({u64(case['low'])}, {u64(case['high'])}, &value)"
else:
raise SystemExit(f"cannot generate C for operation: {operation}")
c_lines.append(f"\terror = {call};")
c_lines.append(f"\tif (error != {expected_error}) {{")
c_lines.append(f"\t\tfprintf(stderr, \"{name}: error=%d\\n\", error);")
c_lines.append("\t\tfailures++;\n\t}")
if case["status"] == "PASS" and "expected" in case:
c_lines.append(f"\tif (value != {u64(case['expected'])}) {{")
c_lines.append(f"\t\tfprintf(stderr, \"{name}: value mismatch\\n\");")
c_lines.append("\t\tfailures++;\n\t}")
if operation == "index_base":
c_lines.append("\t}")
c_lines.extend([
"\tif (failures != 0)",
"\t\treturn (1);",
f"\tprintf(\"B22 extracted arithmetic PASS cases={len(cases)}\\n\");",
"\treturn (0);",
"}",
])
c_path = artifacts / "B22-zmap-arithmetic.c"
binary = artifacts / "B22-zmap-arithmetic"
c_path.write_text("\n".join(c_lines) + "\n", encoding="ascii")
compiled = subprocess.run(
["cc", "-std=gnu11", "-Wall", "-Wextra", "-Werror", str(c_path), "-o", str(binary)],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
(artifacts / "B22-compile.stdout").write_text(compiled.stdout, encoding="utf-8")
(artifacts / "B22-compile.stderr").write_text(compiled.stderr, encoding="utf-8")
if compiled.returncode != 0:
raise SystemExit("B22 extracted arithmetic compilation failed")
executed = subprocess.run(
[str(binary)],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
(artifacts / "B22-extracted.stdout").write_text(executed.stdout, encoding="utf-8")
(artifacts / "B22-extracted.stderr").write_text(executed.stderr, encoding="utf-8")
if executed.returncode != 0:
raise SystemExit("B22 extracted arithmetic execution failed")
result = {
"status": "PASS",
"batch": "B22",
"test": "TC170-zmap-arithmetic",
"baseline": baseline,
"case_count": len(cases),
"negative_count": sum(case["status"] == "EINTEGRITY" for case in cases),
"target_markers": sorted(markers),
"legacy_mismatches": sorted(legacy_mismatches),
"freebsd_positive_errno": True,
"linux_algorithm_locations_audited": True,
"provider_io_calls_unchanged": True,
"metadata_release_calls_unchanged": True,
"decompressor_abi_unchanged": True,
"ondisk_abi_unchanged": True,
"qemu_scope": "exact-source KLD load plus extracted helper replay when PRE15_MODE=qemu",
"full_feature_suite": "NOT_RUN",
}
(artifacts / "B22-decoded-cases.json").write_text(
json.dumps(decoded, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
encoding="ascii",
)
(artifacts / "B22-result.json").write_text(
json.dumps(result, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
encoding="ascii",
)
print(
f"B22 host PASS cases={len(cases)} negatives={result['negative_count']} "
f"markers={len(markers)}"
)
PY
then
:
else
pre15_dut_fail 'B22 source, fixture, or extracted arithmetic check failed'
fi
sha256sum "$artifacts/B22-zmap-arithmetic.c" \
"$artifacts/B22-zmap-arithmetic" \
"$artifacts/B22-decoded-cases.json" "$artifacts/B22-result.json" \
> "$artifacts/SHA256SUMS"
if test "${PRE15_MODE:-host}" != qemu; then
printf '%s\n' \
'TC170 host extracted zmap arithmetic and fixture oracle PASS' \
'QEMU exact-source KLD load and FreeBSD helper replay NOT_RUN in host mode'
exit 0
fi
for tool in awk clang file nm scp; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B22 QEMU tool: $tool"
done
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
module=$PRE15_CASE_TMP/B22-erofs.ko
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
"$PRE15_CASE_TMP/kld-work" >"$artifacts/B22-kld-build.stdout" \
2>"$artifacts/B22-kld-build.stderr"; then
pre15_dut_fail 'B22 cross-target KLD build failed'
fi
pre15_record_module "$module"
file "$module" > "$artifacts/B22-kld-file.txt"
sha256sum "$module" > "$artifacts/B22-kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort > "$artifacts/B22-kld-nm-u.txt"
pre15_scp()
{
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
}
if ! pre15_scp "$module" /root/B22-erofs.ko || \
! pre15_scp "$artifacts/B22-zmap-arithmetic.c" \
/root/B22-zmap-arithmetic.c; then
pre15_infra_blocked 'could not transfer B22 module or arithmetic source'
fi
if pre15_guest_ssh_bounded kldstat -n erofs >/dev/null 2>&1; then
pre15_infra_blocked 'FreeBSD guest already has an EROFS module loaded'
fi
if ! pre15_guest_ssh_bounded kldload /root/B22-erofs.ko \
>"$artifacts/B22-kldload.stdout" 2>"$artifacts/B22-kldload.stderr"; then
if grep -q 'module already loaded or in kernel' \
"$artifacts/B22-kldload.stderr"; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
pre15_dut_fail 'B22 exact-source KLD failed to load'
fi
pre15_own_guest_kld erofs 'B22 exact-source KLD'
if ! pre15_guest_ssh_bounded cc -std=gnu11 -Wall -Wextra -Werror \
/root/B22-zmap-arithmetic.c -o /root/B22-zmap-arithmetic; then
pre15_infra_blocked 'could not compile B22 arithmetic helper in the guest'
fi
if ! pre15_guest_ssh_bounded /root/B22-zmap-arithmetic \
>"$artifacts/B22-guest-arithmetic.stdout" \
2>"$artifacts/B22-guest-arithmetic.stderr"; then
pre15_dut_fail 'B22 FreeBSD arithmetic helper replay failed'
fi
pre15_guest_ssh_bounded kldstat -n erofs > "$artifacts/B22-kldstat.txt"
printf '%s\n' \
'TC170 QEMU PASS exact-source KLD load and FreeBSD arithmetic helper replay' \
'No full feature suite or shared GEOM provider was used'
+412
View File
@@ -0,0 +1,412 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=083acc65f842c409c5a2e089e50060d0062b677c
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
spec=$fixture_dir/B23-explicit-spec.json
generator=$fixture_dir/B23-explicit-generate.py
oracle=$fixture_dir/B23-explicit-oracle.py
probe=$fixture_dir/B23-explicit-probe.c
kld_builder=$fixture_dir/B23-build-kld.sh
zmap=$PRE15_DUT/src/zmap.c
linux_zmap=$PRE15_ROOT/src-linux/zmap.c
artifacts=$PRE15_RUN_DIR/artifacts
first=$PRE15_CASE_TMP/first
second=$PRE15_CASE_TMP/second
for tool in cc diff git mkfs.erofs python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B23 host tool: $tool"
done
pre15_record_fixture b23-spec "$spec"
pre15_record_fixture b23-generator "$generator"
pre15_record_fixture b23-oracle "$oracle"
pre15_record_fixture b23-probe "$probe"
pre15_record_fixture b23-kld-builder "$kld_builder"
pre15_record_fixture b23-case "$PRE15_DUT/tests/pre15/cases/B23-explicit-table.sh"
pre15_record_fixture b23-zmap "$zmap"
pre15_record_fixture b23-linux-zmap "$linux_zmap"
mkdir -p "$artifacts"
if ! python3 -B "$generator" --spec "$spec" --output "$first" \
>"$artifacts/B23-generate-first.stdout" \
2>"$artifacts/B23-generate-first.stderr"; then
pre15_runner_fail 'B23 first fixture generation failed'
fi
if ! python3 -B "$generator" --spec "$spec" --output "$second" \
>"$artifacts/B23-generate-second.stdout" \
2>"$artifacts/B23-generate-second.stderr"; then
pre15_runner_fail 'B23 repeat fixture generation failed'
fi
if ! diff -u "$first/SHA256SUMS" "$second/SHA256SUMS" \
>"$artifacts/B23-repeat-sums.diff" || \
! diff -u "$first/fixture-manifest.json" "$second/fixture-manifest.json" \
>"$artifacts/B23-repeat-manifest.diff"; then
pre15_runner_fail 'B23 fixture generation is not byte deterministic'
fi
if ! python3 -B "$oracle" --spec "$spec" --fixtures "$first" \
--report "$artifacts/B23-oracle-first.json" \
>"$artifacts/B23-oracle-first.stdout" \
2>"$artifacts/B23-oracle-first.stderr"; then
pre15_runner_fail 'B23 independent fixture oracle failed'
fi
if ! python3 -B "$oracle" --spec "$spec" --fixtures "$second" \
--report "$artifacts/B23-oracle-second.json" \
>"$artifacts/B23-oracle-second.stdout" \
2>"$artifacts/B23-oracle-second.stderr"; then
pre15_runner_fail 'B23 repeat fixture oracle failed'
fi
cp "$first/SHA256SUMS" "$artifacts/B23-fixture-SHA256SUMS"
cp "$first/fixture-manifest.json" "$artifacts/B23-fixture-manifest.json"
pre15_record_fixture b23-generated-sums "$artifacts/B23-fixture-SHA256SUMS"
if ! cc -std=c11 -Wall -Wextra -Werror -c "$probe" \
-o "$PRE15_CASE_TMP/B23-explicit-probe.o" \
>"$artifacts/B23-probe-compile.stdout" \
2>"$artifacts/B23-probe-compile.stderr"; then
pre15_runner_fail 'B23 guest probe does not compile on the host'
fi
if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$spec" \
"$artifacts/B23-source-audit.json" <<'PY'
from __future__ import annotations
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
baseline = sys.argv[3]
spec_path = Path(sys.argv[4])
report_path = Path(sys.argv[5])
def committed(path: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
raise SystemExit(f"cannot read B23 baseline {path}: {completed.stderr}")
return completed.stdout
def function(source: str, name: str) -> str:
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if match is None:
raise SystemExit(f"missing function: {name}")
start = source.rfind("\n", 0, source.rfind("\n", 0, match.start())) + 1
brace = source.find("{", match.end())
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
raise SystemExit(f"unterminated function: {name}")
spec = json.loads(spec_path.read_text(encoding="ascii"))
current = (dut / "src/zmap.c").read_text(encoding="utf-8")
before = committed("src/zmap.c")
validator = function(current, "z_erofs_validate_extent_table")
old_validator = function(before, "z_erofs_validate_extent_table")
if validator == old_validator:
raise SystemExit("B23 validator did not change")
if function(current, "z_erofs_read_extent") != function(before, "z_erofs_read_extent"):
raise SystemExit("B23 changed the local record decoder")
for name in ("z_erofs_map_blocks_ext", "z_erofs_fill_inode"):
if function(current, name) != function(before, name):
raise SystemExit(f"B23 changed {name}")
if "#define Z_EROFS_EXTENT_VALIDATE_CHUNK_SIZE (64 * 1024)" not in current:
raise SystemExit("B23 fixed validation chunk bound is missing")
required = (
"z_erofs_read_extent(sbi, vi, record_pos, recsz, &ext)",
"while (scan_pos < table_end)",
"Z_EROFS_EXTENT_VALIDATE_CHUNK_SIZE",
"erofs_read_metadata(sbi, vi->nid, scan_pos, chunk_len, &buf)",
"memcpy(&ext, (const char *)buf.data + offset, recsz)",
"z_erofs_extent_lstart(&ext, recsz)",
"erofs_put_metabuf(&buf)",
"return (index == vi->z_extents ? 0 : EINTEGRITY)",
)
if not all(marker in validator for marker in required):
raise SystemExit("B23 validator is missing a bounded-scan contract marker")
if validator.count("z_erofs_read_extent(") != 1:
raise SystemExit("B23 short tail probe count changed")
if validator.count("erofs_read_metadata(") != 1:
raise SystemExit("B23 long-table reader is not chunk-scoped")
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", validator):
raise SystemExit("B23 introduced Linux negative errno")
allowed = {
"repo-pre-15/src/zmap.c",
"repo-pre-15/tests/pre15/cases/B23-explicit-table.sh",
"repo-pre-15/tests/pre15/fixtures/B23-build-kld.sh",
"repo-pre-15/tests/pre15/fixtures/B23-explicit-generate.py",
"repo-pre-15/tests/pre15/fixtures/B23-explicit-oracle.py",
"repo-pre-15/tests/pre15/fixtures/B23-explicit-probe.c",
"repo-pre-15/tests/pre15/fixtures/B23-explicit-spec.json",
}
changed = set(
subprocess.run(
["git", "-C", str(root), "diff", "--name-only", baseline, "--", "repo-pre-15"],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()
)
changed.update(
subprocess.run(
[
"git", "-C", str(root), "ls-files", "--others",
"--exclude-standard", "--", "repo-pre-15",
],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()
)
if changed != allowed:
raise SystemExit(f"B23 write set differs: {sorted(changed ^ allowed)}")
for path in (
"src/erofs_fs.h",
"src/data.c",
"src/decompressor.c",
"src/decompressor_lz4.c",
"src/decompressor_lzma.c",
"src/decompressor_deflate.c",
"src/decompressor_zstd.c",
"src/internal.h",
):
if (dut / path).read_text(encoding="utf-8") != committed(path):
raise SystemExit(f"B23 changed an excluded provider/codec/ABI file: {path}")
linux = (root / "src-linux/zmap.c").read_text(encoding="utf-8")
for marker in (
"recsz <= offsetof(struct z_erofs_extent, pstart_hi)",
"le64_to_cpu(*(__le64 *)ext)",
"le32_to_cpu(ext->pstart_lo)",
"le32_to_cpu(ext->pstart_hi) << 32",
"le32_to_cpu(ext->lstart_hi) << 32",
"erofs_inode_in_metabox(inode)",
):
if marker not in linux:
raise SystemExit(f"Linux explicit-table anchor changed: {marker}")
report = {
"baseline": baseline,
"batch": "B23",
"candidates": spec["candidates"],
"chunk_size": spec["chunk_size"],
"decompressor_files_unchanged": True,
"freebsd_positive_errno": True,
"linux_record_decode_audited": True,
"ondisk_header_unchanged": True,
"primary_metabox_reader_unchanged": True,
"status": "PASS",
"test": spec["test"],
"write_set": sorted(changed),
}
report_path.write_text(
json.dumps(report, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
encoding="ascii",
)
print("B23 source/write-set/Linux-FreeBSD audit PASS")
PY
then
:
else
pre15_dut_fail 'B23 source, write-set, or cross-tree audit failed'
fi
sha256sum "$artifacts/B23-fixture-SHA256SUMS" \
"$artifacts/B23-fixture-manifest.json" \
"$artifacts/B23-oracle-first.json" \
"$artifacts/B23-oracle-second.json" \
"$artifacts/B23-source-audit.json" \
>"$artifacts/SHA256SUMS"
pre15_target_reached
if test "${PRE15_MODE:-host}" != qemu; then
printf '%s\n' \
'TC171 host real-fixture, independent oracle, and bounded source audit PASS' \
'QEMU exact-source mapped/backing runtime NOT_RUN in host mode' \
'Full feature suite NOT_RUN'
exit 0
fi
for tool in awk clang file nm scp tar; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B23 QEMU tool: $tool"
done
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
fixture_archive=$PRE15_CASE_TMP/B23-fixtures.tar.gz
module=$PRE15_CASE_TMP/B23-erofs.ko
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
"$PRE15_CASE_TMP/kld-work" >"$artifacts/B23-kld-build.stdout" \
2>"$artifacts/B23-kld-build.stderr"; then
pre15_dut_fail 'B23 cross-target zstdio0 KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/B23-kld-file.txt"
sha256sum "$module" >"$artifacts/B23-kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/B23-kld-nm-u.txt"
tar -C "$first" -czf "$fixture_archive" .
pre15_scp()
{
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
}
if ! pre15_scp "$fixture_archive" /root/B23-fixtures.tar.gz || \
! pre15_scp "$probe" /root/B23-explicit-probe.c || \
! pre15_scp "$module" /root/B23-erofs.ko; then
pre15_infra_blocked 'could not transfer B23 module, probe, or fixtures'
fi
if ! pre15_guest_ssh_bounded \
'rm -rf /root/pre15-b23-fixtures && mkdir /root/pre15-b23-fixtures && tar -xzf /root/B23-fixtures.tar.gz -C /root/pre15-b23-fixtures'; then
pre15_infra_blocked 'could not prepare B23 guest fixtures'
fi
if pre15_guest_ssh_bounded kldstat -n erofs >/dev/null 2>&1; then
pre15_infra_blocked 'FreeBSD guest already has an EROFS module loaded'
fi
if ! pre15_guest_ssh_bounded kldload /root/B23-erofs.ko \
>"$artifacts/B23-kldload.stdout" 2>"$artifacts/B23-kldload.stderr"; then
if grep -q 'module already loaded or in kernel' "$artifacts/B23-kldload.stderr"; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
pre15_dut_fail 'B23 exact-source KLD failed to load'
fi
pre15_own_guest_kld erofs 'B23 exact-source KLD'
if ! pre15_guest_ssh_bounded cc -std=c11 -Wall -Wextra -Werror \
-o /root/B23-explicit-probe /root/B23-explicit-probe.c; then
pre15_infra_blocked 'could not compile the B23 guest probe'
fi
python3 -B - "$first/fixture-manifest.json" >"$PRE15_CASE_TMP/B23-qemu-cases.tsv" <<'PY'
import json
from pathlib import Path
import sys
manifest = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
for name, case in sorted(manifest["cases"].items()):
offsets = ",".join(str(value) for value in case["probe_offsets"])
print(
name,
case["image"],
case["expected_errno"],
case["file_size"],
offsets,
case["target_marker"],
sep="\t",
)
PY
pre15_attach_md()
{
pre15_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode -f "$1") || \
pre15_dut_fail "could not attach B23 provider: $1"
case "$pre15_md" in
md[0-9]*) ;;
*) pre15_runner_fail "unexpected mdconfig output: $pre15_md" ;;
esac
pre15_md=${pre15_md#md}
pre15_own_guest_md "$pre15_md" "$2"
printf '%s\n' "$pre15_md"
}
while IFS="$(printf '\t')" read -r case_id image_name expected_errno \
file_size offsets target_marker; do
mountpoint=/mnt/pre15-b23-$case_id
unit=$(pre15_attach_md "/root/pre15-b23-fixtures/$image_name" \
"B23 $case_id primary")
pre15_guest_ssh_bounded mkdir -p "$mountpoint"
if ! pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/md$unit" \
"$mountpoint"; then
pre15_dut_fail "B23 $case_id mount failed before $target_marker"
fi
pre15_own_guest_mount "$mountpoint" "B23 $case_id mount"
if test "$expected_errno" -eq 0; then
old_ifs=$IFS
IFS=,
set -- $offsets
IFS=$old_ifs
if ! pre15_guest_ssh_bounded /root/B23-explicit-probe pass \
"$mountpoint/target.bin" "$file_size" "$@"; then
pre15_dut_fail "B23 mapped positive failed at $target_marker"
fi
else
if ! pre15_guest_ssh_bounded /root/B23-explicit-probe errno \
"$mountpoint/target.bin" "$expected_errno"; then
pre15_dut_fail "B23 corruption result failed at $target_marker"
fi
fi
done <"$PRE15_CASE_TMP/B23-qemu-cases.tsv"
pre15_guest_ssh_bounded dmesg >"$artifacts/B23-dmesg.txt"
if grep -Eq 'panic:|Fatal trap|lock order reversal|KDB: stack backtrace' \
"$artifacts/B23-dmesg.txt"; then
pre15_dut_fail 'B23 runtime produced a kernel diagnostic'
fi
printf '%s\n' \
'TC171 QEMU explicit primary/metabox mapped and corruption runtime PASS' \
'All errno values are positive FreeBSD ABI values; full feature suite NOT_RUN'
+337
View File
@@ -0,0 +1,337 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=fb79d17edba7c47fae6e60b2a92771a8f3a2fcdf
zmap=$PRE15_DUT/src/zmap.c
internal=$PRE15_DUT/src/internal.h
linux_zmap=$PRE15_ROOT/src-linux/zmap.c
oracle=$PRE15_DUT/tests/pre15/fixtures/B07-map-oracle.json
gate_input=$PRE15_DUT/tests/pre15/gates/P15-006-input.json
artifacts=$PRE15_RUN_DIR/artifacts
for tool in git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B24 host tool: $tool"
done
pre15_record_fixture b24-zmap "$zmap"
pre15_record_fixture b24-internal "$internal"
pre15_record_fixture b24-linux-zmap "$linux_zmap"
pre15_record_fixture b24-map-oracle "$oracle"
pre15_record_fixture b24-gate-input "$gate_input"
pre15_record_fixture b24-case \
"$PRE15_DUT/tests/pre15/cases/B24-zmap-order.sh"
mkdir -p "$artifacts"
pre15_target_reached
if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$oracle" \
"$gate_input" "$artifacts/B24-zmap-order.json" <<'PY'
from __future__ import annotations
from collections import Counter
import hashlib
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
baseline = sys.argv[3]
oracle_path = Path(sys.argv[4])
gate_input_path = Path(sys.argv[5])
report_path = Path(sys.argv[6])
def committed(path: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
raise SystemExit(f"cannot read B24 baseline {path}: {completed.stderr}")
return completed.stdout
def function(source: str, name: str) -> str:
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if match is None:
raise SystemExit(f"missing function: {name}")
name_line = source.rfind("\n", 0, match.start()) + 1
start = source.rfind("\n", 0, name_line - 1) + 1
brace = source.find("{", match.end())
if brace < 0:
raise SystemExit(f"missing function body: {name}")
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
raise SystemExit(f"unterminated function: {name}")
function_order = [
"z_erofs_index_base",
"z_erofs_index_advance",
"z_erofs_lcluster_count",
"z_erofs_lcluster_pos",
"z_erofs_lcn_advance",
"z_erofs_compact_pblk",
"z_erofs_fragment_offset",
"z_erofs_post_eof_len",
"z_erofs_physical_end",
"z_erofs_read_index",
"z_erofs_load_full_lcluster",
"decode_compactedbits",
"get_compacted_la_distance",
"z_erofs_load_compact_lcluster",
"z_erofs_load_lcluster_from_disk",
"z_erofs_extent_lookback",
"z_erofs_get_extent_compressedlen",
"z_erofs_get_extent_decompressedlen",
"z_erofs_extent_add",
"z_erofs_extent_roundup",
"z_erofs_extent_table_pos",
"z_erofs_extent_record_pos",
"z_erofs_read_extent",
"z_erofs_extent_lstart",
"z_erofs_validate_extent_table",
"z_erofs_map_blocks_fo",
"z_erofs_map_blocks_ext",
"z_erofs_fill_inode",
"z_erofs_map_sanity_check",
"z_erofs_map_blocks",
]
linux_algorithm_order = [
"z_erofs_load_full_lcluster",
"decode_compactedbits",
"get_compacted_la_distance",
"z_erofs_load_compact_lcluster",
"z_erofs_load_lcluster_from_disk",
"z_erofs_extent_lookback",
"z_erofs_get_extent_compressedlen",
"z_erofs_get_extent_decompressedlen",
"z_erofs_map_blocks_fo",
"z_erofs_map_blocks_ext",
"z_erofs_fill_inode",
"z_erofs_map_sanity_check",
]
current = (dut / "src/zmap.c").read_text(encoding="utf-8")
before = committed("src/zmap.c")
current_internal = (dut / "src/internal.h").read_text(encoding="utf-8")
before_internal = committed("src/internal.h")
linux = (root / "src-linux/zmap.c").read_text(encoding="utf-8")
moved_start = before.index("static int\nz_erofs_extent_add(")
moved_end = before.index("static int\nz_erofs_map_blocks_ext(", moved_start)
moved = before[moved_start:moved_end]
without_moved = before[:moved_start] + before[moved_end:]
insert_at = without_moved.index("static int\nz_erofs_map_blocks_fo(")
expected = without_moved[:insert_at] + moved + without_moved[insert_at:]
if current != expected:
raise SystemExit("zmap.c differs from the exact B24 validator-group move")
before_functions = {name: function(before, name) for name in function_order}
current_functions = {name: function(current, name) for name in function_order}
for name in function_order:
if current_functions[name] != before_functions[name]:
raise SystemExit(f"B24 changed function text instead of moving it: {name}")
def definition_position(source: str, name: str) -> int:
match = re.search(
r"^(?:" + re.escape(name) + r"|(?:static\s+)?"
r"[A-Za-z_][A-Za-z0-9_ *]*\b" + re.escape(name) + r")\s*\(",
source,
re.MULTILINE,
)
if match is None:
raise SystemExit(f"missing definition position: {name}")
return match.start()
positions = [definition_position(current, name) for name in function_order]
if positions != sorted(positions):
raise SystemExit("B24 zmap definition order does not match the reviewed order")
for source, label in ((current, "FreeBSD"), (linux, "Linux")):
positions = [definition_position(source, name) for name in linux_algorithm_order]
if positions != sorted(positions):
raise SystemExit(f"{label} common zmap algorithm order drifted")
def direct_calls(functions: dict[str, str]) -> Counter[tuple[str, str]]:
calls: Counter[tuple[str, str]] = Counter()
for caller, body in functions.items():
brace = body.find("{")
for callee in re.findall(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(", body[brace + 1 :]):
if callee not in {"if", "for", "while", "switch", "return", "sizeof"}:
calls[(caller, callee)] += 1
return calls
before_calls = direct_calls(before_functions)
current_calls = direct_calls(current_functions)
if current_calls != before_calls:
raise SystemExit("B24 changed the direct-call multiset")
global_definitions = sorted(
name for name, body in current_functions.items() if not body.startswith("static ")
)
if global_definitions != ["z_erofs_fill_inode", "z_erofs_map_blocks"]:
raise SystemExit(f"unexpected zmap global definitions: {global_definitions}")
if current_internal != before_internal:
raise SystemExit("B24 changed internal.h despite no removable zmap global")
inode_source = (dut / "src/inode.c").read_text(encoding="utf-8")
data_source = (dut / "src/data.c").read_text(encoding="utf-8")
if inode_source.count("z_erofs_fill_inode(sbi, vi)") != 1:
raise SystemExit("eager z_erofs_fill_inode no longer has its inode consumer")
if data_source.count("z_erofs_map_blocks(sbi, vi, &next)") != 1:
raise SystemExit("compressed map backend no longer has its common adapter consumer")
for prototype in (
"int z_erofs_fill_inode(struct erofs_sb_info *sbi, struct erofs_inode *vi);",
"int z_erofs_map_blocks(struct erofs_sb_info *sbi, struct erofs_inode *vi,\n"
" struct erofs_map_blocks *map);",
):
if current_internal.count(prototype) != 1:
raise SystemExit(f"required FreeBSD zmap prototype drifted: {prototype!r}")
if "int z_erofs_map_blocks_iter(struct inode *inode, struct erofs_map_blocks *map," not in linux:
raise SystemExit("Linux zmap iterator naming anchor changed")
for marker in ("erofs_read_metadata(", "erofs_put_metabuf(", "goto out;"):
if current.count(marker) != before.count(marker):
raise SystemExit(f"B24 changed provider I/O or cleanup marker count: {marker}")
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", current):
raise SystemExit("B24 introduced Linux negative errno")
oracle = json.loads(oracle_path.read_text(encoding="ascii"))
gate_input = json.loads(gate_input_path.read_text(encoding="ascii"))
records = oracle.get("records", [])
if oracle.get("map_case_count") != 80 or len(records) != 80:
raise SystemExit("frozen G02/B07 map tuple denominator changed")
if gate_input.get("expected_device_case_count") != 13:
raise SystemExit("frozen G02/B07 device denominator changed")
tuple_bytes = b"".join(bytes.fromhex(record["tuple_hex"]) for record in records)
tuple_digest = hashlib.sha256(tuple_bytes).hexdigest()
if tuple_digest != oracle.get("tuple_bytes_sha256"):
raise SystemExit("frozen G02/B07 tuple bytes no longer match their digest")
if len({record["id"] for record in records}) != 80:
raise SystemExit("frozen G02/B07 tuple IDs are not unique")
changed = set(
subprocess.run(
["git", "-C", str(root), "diff", "--name-only", baseline, "--", "repo-pre-15"],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()
)
changed.update(
subprocess.run(
[
"git", "-C", str(root), "ls-files", "--others", "--exclude-standard",
"--", "repo-pre-15",
],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()
)
allowed = {
"repo-pre-15/src/zmap.c",
"repo-pre-15/tests/pre15/cases/B24-zmap-order.sh",
}
if changed != allowed:
raise SystemExit(f"B24 write set differs: {sorted(changed ^ allowed)}")
function_digest = hashlib.sha256(
"\n\n".join(current_functions[name] for name in function_order).encode("utf-8")
).hexdigest()
report = {
"baseline": baseline,
"batch": "B24",
"candidate": "P15-016",
"direct_call_edges": sum(current_calls.values()),
"freebsd_backend_name": "z_erofs_map_blocks",
"freebsd_positive_errno": True,
"function_count": len(function_order),
"function_text_sha256": function_digest,
"global_definitions": global_definitions,
"linux_iterator_name": "z_erofs_map_blocks_iter",
"map_cases": len(records),
"device_cases": gate_input["expected_device_case_count"],
"provider_io_unchanged": True,
"metadata_cleanup_unchanged": True,
"status": "PASS",
"tuple_bytes_sha256": tuple_digest,
"write_set": sorted(changed),
}
report_path.write_text(
json.dumps(report, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
encoding="ascii",
)
print(
"B24 order/callgraph PASS "
f"functions={len(function_order)} calls={sum(current_calls.values())} globals=2"
)
print(
"B24 map tuples PASS "
f"maps={len(records)} devices={gate_input['expected_device_case_count']} "
f"sha256={tuple_digest}"
)
PY
then
:
else
pre15_dut_fail 'B24 order, callgraph, visibility, or tuple proof failed'
fi
sha256sum "$artifacts/B24-zmap-order.json" >"$artifacts/SHA256SUMS"
printf '%s\n' \
'B24 host PASS exact validator move and Linux algorithm order' \
'B24 host PASS frozen G02 map tuples; QEMU and full feature suite NOT_RUN'
+37
View File
@@ -0,0 +1,37 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=c750850264cbc3c52ef4407aaf0c8d02ec26e973
fixture=$PRE15_DUT/tests/pre15/fixtures/B25-codec-errors.json
oracle=$PRE15_DUT/tests/pre15/fixtures/B25-codec-oracle.py
artifacts=$PRE15_RUN_DIR/artifacts
for tool in cc git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B25 host tool: $tool"
done
pre15_record_fixture b25-case "$PRE15_DUT/tests/pre15/cases/B25-codec-errors.sh"
pre15_record_fixture b25-fixture "$fixture"
pre15_record_fixture b25-oracle "$oracle"
for source in compress.h decompressor.c decompressor_lz4.c \
decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c; do
pre15_record_fixture "b25-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
pre15_target_reached
if python3 -B "$oracle" --root "$PRE15_ROOT" --dut "$PRE15_DUT" \
--baseline "$baseline" --fixture "$fixture" --artifacts "$artifacts"
then
pre15_record_fixture b25-report "$artifacts/B25-codec-errors.json"
else
pre15_dut_fail 'B25 typed codec errno oracle failed'
fi
+680
View File
@@ -0,0 +1,680 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
spec=$fixture_dir/B27-stream-tail.json
generator=$fixture_dir/B27-stream-fixtures.py
probe=$fixture_dir/B27-stream-probe.c
compact_probe=$fixture_dir/B27-compact-finalization.c
kld_builder=$fixture_dir/B27-build-kld.sh
gate_script=$PRE15_DUT/tests/pre15/gates/P15-083.sh
gate_input=$PRE15_DUT/tests/pre15/gates/P15-083-input.json
artifacts=$PRE15_RUN_DIR/artifacts
gate_clone=$PRE15_CASE_TMP/gate-clone
gate_output=$PRE15_CASE_TMP/gate-output
first=$PRE15_CASE_TMP/fixtures-first
second=$PRE15_CASE_TMP/fixtures-second
for tool in cc cmp diff dump.erofs fsck.erofs git mkfs.erofs python3 sha256sum timeout; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B27 host tool: $tool"
done
pre15_record_fixture b27-case "$PRE15_DUT/tests/pre15/cases/B27-stream-tail.sh"
pre15_record_fixture b27-spec "$spec"
pre15_record_fixture b27-generator "$generator"
pre15_record_fixture b27-probe "$probe"
pre15_record_fixture b27-compact-finalization "$compact_probe"
pre15_record_fixture b27-kld-builder "$kld_builder"
pre15_record_fixture b27-gate "$gate_script"
pre15_record_fixture b27-gate-input "$gate_input"
for source in decompressor_deflate.c decompressor_lzma.c decompressor_zstd.c \
decompressor.c zdata.c zmap.c compress.h; do
pre15_record_fixture "b27-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
if ! git clone -q --shared --no-checkout "$PRE15_ROOT" "$gate_clone" \
>"$artifacts/gate-clone.stdout" 2>"$artifacts/gate-clone.stderr" || \
! git -C "$gate_clone" checkout -q --detach \
338ba8daf81c1461b5d28ca9c5345686e4e0f2eb \
>"$artifacts/gate-checkout.stdout" 2>"$artifacts/gate-checkout.stderr"; then
pre15_runner_fail 'could not materialize the frozen P15-083 gate commit'
fi
frozen_gate=$gate_clone/repo-pre-15/tests/pre15/gates/P15-083.sh
if ! (umask 022 && timeout -k 10 240 "$frozen_gate" \
--base 68bbe94c44e35d53cec8ab55d007f40b01cf0502 \
--output "$gate_output") >"$artifacts/gate.stdout" \
2>"$artifacts/gate.stderr"; then
pre15_runner_fail 'frozen P15-083 gate replay failed'
fi
if ! (umask 022 && python3 -B "$generator" --spec "$spec" --output "$first") \
>"$artifacts/generate-first.json" 2>"$artifacts/generate-first.stderr"; then
pre15_runner_fail 'B27 first real EROFS fixture generation failed'
fi
if ! (umask 022 && python3 -B "$generator" --spec "$spec" --output "$second") \
>"$artifacts/generate-second.json" 2>"$artifacts/generate-second.stderr"; then
pre15_runner_fail 'B27 second real EROFS fixture generation failed'
fi
if ! diff -qr "$first" "$second" >"$artifacts/fixture-repeat.diff"; then
pre15_runner_fail 'B27 real EROFS fixtures are not byte reproducible'
fi
cp "$gate_output/result.json" "$artifacts/P15-083-result.json"
cp "$gate_output/codec-results.json" "$artifacts/P15-083-codec-results.json"
cp "$first/fixture-index.json" "$artifacts/B27-fixture-index.json"
if ! cc -std=c11 -Wall -Wextra -Werror "$compact_probe" \
-o "$PRE15_CASE_TMP/B27-compact-finalization" \
>"$artifacts/B27-compact-finalization-build.stdout" \
2>"$artifacts/B27-compact-finalization-build.stderr" || \
! "$PRE15_CASE_TMP/B27-compact-finalization" \
>"$artifacts/B27-compact-finalization.stdout" \
2>"$artifacts/B27-compact-finalization.stderr"; then
pre15_dut_fail 'B27 compact HEAD finalization regression failed'
fi
if ! python3 -B - "$PRE15_DUT/src/zmap.c" \
"$PRE15_DUT/src/decompressor_zstd.c" \
"$artifacts/B27-compact-finalization-source.json" <<'PY'
import json
from pathlib import Path
import sys
source = Path(sys.argv[1]).read_text(encoding="utf-8")
zstd = Path(sys.argv[2]).read_text(encoding="utf-8")
rejected = """\t\t\t\tif ((lo & Z_EROFS_LI_D0_CBLKCNT) != 0) {
\t\t\t\t\tif (i == 0) {
\t\t\t\t\t\terofs_put_metabuf(&buf);
\t\t\t\t\t\treturn (EINTEGRITY);
\t\t\t\t\t}
"""
accepted = """\t\t\t\tif ((lo & Z_EROFS_LI_D0_CBLKCNT) != 0) {
\t\t\t\t\t--i;
\t\t\t\t\tnblk += lo & ~Z_EROFS_LI_D0_CBLKCNT;
\t\t\t\t\tcontinue;
"""
if rejected in source or source.count(accepted) != 1:
raise SystemExit("B27 compact HEAD finalization source mismatch")
if zstd.count("\t.supports_subextent = 0,\n") != 1:
raise SystemExit("B27 Zstd ordinary subextent fallback is disabled")
Path(sys.argv[3]).write_text(json.dumps({
"cblkcnt_first_slot": "accepted",
"linux_completion_semantics": True,
"status": "PASS",
"zstd_ordinary_subextent": False,
"zstd_partial_reference_completion": True,
}, indent=2, sort_keys=True) + "\n", encoding="ascii")
PY
then
pre15_dut_fail 'B27 compact HEAD finalization source audit failed'
fi
if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
pre15_target_reached
else
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$spec" \
"$gate_script" "$gate_input" "$gate_output/result.json" \
"$gate_output/codec-results.json" "$first/fixture-index.json" \
"$artifacts/B27-source-audit.json" "$PRE15_CASE_TMP/finish-harness.c" <<'PY'
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
spec = json.loads(Path(sys.argv[3]).read_text(encoding="ascii"))
gate_script = Path(sys.argv[4])
gate_input = Path(sys.argv[5])
result = json.loads(Path(sys.argv[6]).read_text(encoding="ascii"))
codec_results = json.loads(Path(sys.argv[7]).read_text(encoding="ascii"))
fixture_index = json.loads(Path(sys.argv[8]).read_text(encoding="ascii"))
report_path = Path(sys.argv[9])
harness_path = Path(sys.argv[10])
baseline = spec["baseline"]
implementation = "0eda2fcfc94aba1e51d3f9ee9885260969240de1"
runtime_base = "445da30f913dc292666f340588236498ea793db3"
def fail(message: str) -> None:
raise SystemExit(message)
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def committed(commit: str, name: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{commit}:repo-pre-15/src/{name}"],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if completed.returncode != 0:
fail(f"cannot read B27 source {commit}:{name}: {completed.stderr.strip()}")
return completed.stdout
def replace_once(source: str, old: str, new: str, label: str) -> str:
count = source.count(old)
if count != 1:
fail(f"{label}: expected one baseline occurrence, found {count}")
return source.replace(old, new, 1)
def extract_function(source: str, name: str) -> str:
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if match is None:
fail(f"missing function: {name}")
name_line = source.rfind("\n", 0, match.start()) + 1
start = source.rfind("\n", 0, name_line - 1) + 1
brace = source.find("{", match.end())
depth = 0
for index in range(brace, len(source)):
if source[index] == "{":
depth += 1
elif source[index] == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
fail(f"unterminated function: {name}")
if (
spec.get("schema") != 1
or spec.get("batch") != "B27"
or spec.get("candidate") != "P15-083"
or spec.get("test") != "TC176-stream-runtime"
):
fail("B27 fixture identity changed")
if sha256(gate_script) != spec["gate"]["script_sha256"]:
fail("tracked P15-083 gate script differs from G04 GO")
if sha256(gate_input) != spec["gate"]["input_sha256"]:
fail("tracked P15-083 gate input differs from G04 GO")
if not (
result.get("status") == "GO"
and result.get("b27") == "AUTHORIZED"
and result.get("cleanup") == "PASS"
and result.get("typed_errno") == "PASS"
and result.get("codecs") == {"deflate": "GO", "lzma": "GO", "zstd": "GO"}
):
fail("frozen P15-083 gate did not reproduce B27 authorization")
observed = {item["codec"]: item for item in codec_results}
if set(observed) != set(spec["codecs"]):
fail("G04 codec result set changed")
for codec, expected in spec["codecs"].items():
item = observed[codec]
gate = expected["gate"]
extent = expected["extent"]
if any(item["extent"][key] != extent[key] for key in (
"leading_zero_bytes", "logical_length", "logical_offset",
"physical_length", "physical_offset", "stream_bytes"
)):
fail(f"{codec} legal leading-padding extent changed")
checks = (
item["full"]["consumed"] == gate["full_consumed"],
item["full"]["policy_errno"] == 0,
item["partial"]["consumed"] == gate["partial_consumed"],
item["partial"]["requested_bytes"] == extent["partial_size"],
item["partial"]["policy_errno"] == 0,
item["partial"]["corrupt_policy_errno"] == 0,
item["tail"]["consumed"] == gate["tail_consumed"],
item["tail"]["policy_errno"] == 97,
item["tail"]["fsck"]["exit"] == gate["tail_fsck_exit"],
item["truncated"]["consumed"] == gate["truncated_consumed"],
item["truncated"]["policy_errno"] == 97,
item["corruption"]["full_errno"] == 97,
item["corruption"]["starts_at_stream_byte"] == expected["corruption_start"],
item["corruption"]["starts_after_partial_consumed"] is True,
)
if not all(checks):
fail(f"{codec} G04 completion/trailing/partial contract changed")
for phase in ("full", "partial", "tail", "truncated"):
if item[phase]["cleanup"] != 1 or item[phase]["guards"] != 1:
fail(f"{codec} {phase} oracle cleanup/guard failed")
if fixture_index.get("fixture_count") != 12 or fixture_index.get("status") != "READY":
fail("B27 runtime fixture matrix is incomplete")
classes = {(item["codec"], item["class"]) for item in fixture_index["fixtures"]}
if classes != {
(codec, kind)
for codec in spec["codecs"]
for kind in ("valid", "tail", "truncated", "corrupt")
}:
fail("B27 runtime fixture class set changed")
base = {
name: committed(baseline, name)
for name in (
"compress.h", "decompressor.c", "decompressor_deflate.c",
"decompressor_lzma.c", "decompressor_zstd.c", "zdata.c"
)
}
implemented = {name: committed(implementation, name) for name in base}
for name in ("compress.h", "decompressor.c", "zdata.c"):
if implemented[name] != base[name]:
fail(f"B27 changed excluded provider/GEOM/buffer-lifetime file: {name}")
deflate_helper = '''static int
z_erofs_deflate_finish(const struct z_erofs_decompress_req *rq, int ret,
uInt avail_in)
{
\tif (rq->partial_decoding)
\t\treturn (0);
\tif (ret != Z_STREAM_END || avail_in != 0)
\t\treturn (EINTEGRITY);
\treturn (0);
}
'''
expected = replace_once(
base["decompressor_deflate.c"],
"static int\nz_erofs_deflate_decompress",
deflate_helper + "static int\nz_erofs_deflate_decompress",
"Deflate completion helper",
)
expected = replace_once(
expected,
"\telse if (error == 0 && !rq->partial_decoding &&\n"
"\t (ret != Z_STREAM_END || strm.avail_in != 0))\n"
"\t\terror = EINTEGRITY;",
"\telse if (error == 0)\n"
"\t\terror = z_erofs_deflate_finish(rq, ret, strm.avail_in);",
"Deflate completion call",
)
if implemented["decompressor_deflate.c"] != expected:
fail("Deflate differs from the exact B27 transform")
lzma_helper = '''static int
z_erofs_lzma_finish(const struct z_erofs_decompress_req *rq, enum xz_ret ret,
size_t input_pos)
{
\tif (rq->partial_decoding &&
\t (ret == XZ_OK || ret == XZ_STREAM_END))
\t\treturn (0);
\tif (!rq->partial_decoding && ret == XZ_STREAM_END &&
\t input_pos == rq->inputsize)
\t\treturn (0);
\treturn (z_erofs_lzma_error(ret));
}
'''
expected = replace_once(
base["decompressor_lzma.c"],
"static int\nz_erofs_lzma_decompress",
lzma_helper + "static int\nz_erofs_lzma_decompress",
"LZMA completion helper",
)
expected = replace_once(
expected,
"\telse if (rq->partial_decoding &&\n"
"\t (ret == XZ_OK || ret == XZ_STREAM_END))\n"
"\t\terror = 0;\n"
"\telse if (!rq->partial_decoding && ret == XZ_STREAM_END &&\n"
"\t buffer.in_pos == rq->inputsize)\n"
"\t\terror = 0;\n"
"\telse\n"
"\t\terror = z_erofs_lzma_error(ret);",
"\telse\n"
"\t\terror = z_erofs_lzma_finish(rq, ret, buffer.in_pos);",
"LZMA completion call",
)
if implemented["decompressor_lzma.c"] != expected:
fail("LZMA differs from the exact B27 transform")
zstd_helper = '''static int
z_erofs_zstd_finish(const struct z_erofs_decompress_req *rq, size_t ret,
size_t input_pos, size_t input_size)
{
\tif (rq->partial_decoding)
\t\treturn (0);
\tif (ret != 0 || input_pos != input_size)
\t\treturn (EINTEGRITY);
\treturn (0);
}
'''
expected = replace_once(
base["decompressor_zstd.c"],
"static const ZSTD_customMem zstd_erofs_alloc = {\n"
"\t.customAlloc = zstd_alloc,\n"
"\t.customFree = zstd_free,\n"
"\t.opaque = M_EROFS,\n"
"};\n\n"
"static int\nz_erofs_zstd_decompress",
"static const ZSTD_customMem zstd_erofs_alloc = {\n"
"\t.customAlloc = zstd_alloc,\n"
"\t.customFree = zstd_free,\n"
"\t.opaque = M_EROFS,\n"
"};\n\n" + zstd_helper + "static int\nz_erofs_zstd_decompress",
"Zstd completion helper",
)
expected = replace_once(
expected,
"\telse if (error == 0 && !rq->partial_decoding &&\n"
"\t (ret != 0 || input.pos != input.size))\n"
"\t\terror = EINTEGRITY;",
"\telse if (error == 0)\n"
"\t\terror = z_erofs_zstd_finish(rq, ret, input.pos, input.size);",
"Zstd completion call",
)
if implemented["decompressor_zstd.c"] != expected:
fail("Zstd differs from the exact B27 transform")
runtime_names = (
"decompressor_deflate.c", "decompressor_lzma.c",
"decompressor_zstd.c", "zmap.c",
)
runtime_before = {name: committed(runtime_base, name) for name in runtime_names}
current = {
name: (dut / "src" / name).read_text(encoding="utf-8")
for name in runtime_names
}
if current["decompressor_deflate.c"] != runtime_before["decompressor_deflate.c"]:
fail("B27 remediation changed Deflate beyond the accepted implementation")
if current["decompressor_lzma.c"] != runtime_before["decompressor_lzma.c"]:
fail("B27 remediation changed LZMA beyond the accepted implementation")
if current["decompressor_zstd.c"] != runtime_before["decompressor_zstd.c"]:
fail("B27 remediation Zstd completion differs from the exact transform")
expected = replace_once(
runtime_before["zmap.c"],
"\t\t\t\t\tif (i == 0) {\n"
"\t\t\t\t\t\terofs_put_metabuf(&buf);\n"
"\t\t\t\t\t\treturn (EINTEGRITY);\n"
"\t\t\t\t\t}\n",
"",
"compact HEAD first-slot CBLKCNT completion",
)
if current["zmap.c"] != expected:
fail("B27 remediation compact HEAD completion differs from the exact transform")
changed_source = set(
subprocess.run(
["git", "-C", str(root), "diff", "--name-only", runtime_base, "--", "repo-pre-15/src"],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()
)
if changed_source != {
"repo-pre-15/src/zmap.c",
}:
fail(f"B27 source write set changed: {sorted(changed_source)}")
all_changed = set(
subprocess.run(
["git", "-C", str(root), "diff", "--name-only", runtime_base, "--", "repo-pre-15"],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()
)
all_changed.update(
subprocess.run(
["git", "-C", str(root), "ls-files", "--others", "--exclude-standard", "--", "repo-pre-15"],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()
)
expected_write_set = {
"repo-pre-15/src/zmap.c",
"repo-pre-15/tests/pre15/cases/B27-stream-tail.sh",
"repo-pre-15/tests/pre15/fixtures/B27-compact-finalization.c",
}
if all_changed != expected_write_set:
fail(f"B27 exact write set differs: {sorted(all_changed ^ expected_write_set)}")
joined = "\n".join(current.values())
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", joined):
fail("negative Linux errno entered B27")
if "ZSTD_getErrorCode" in current["decompressor_zstd.c"]:
fail("B27 expanded the optional Zstd provider ABI")
linux = {
codec: (root / f"src-linux/decompressor_{codec}.c").read_text(encoding="utf-8")
for codec in spec["codecs"]
}
for codec, marker in {
"deflate": "if (zerr == Z_STREAM_END && !rq->outputsize)",
"lzma": "xz_dec_microlzma_reset(strm->state, rq->inputsize, rq->outputsize",
"zstd": "zerr = zstd_decompress_stream(stream, &out_buf, &in_buf);",
}.items():
if marker not in linux[codec]:
fail(f"Linux {codec} stream-tail anchor changed")
harness = f'''#include <stddef.h>
#include <stdio.h>
#define EINTEGRITY 97
#define ENOMEM 12
#define EOPNOTSUPP 95
#define Z_STREAM_END 1
typedef unsigned int uInt;
enum xz_ret {{
XZ_OK, XZ_STREAM_END, XZ_UNSUPPORTED_CHECK, XZ_MEM_ERROR,
XZ_MEMLIMIT_ERROR, XZ_FORMAT_ERROR, XZ_OPTIONS_ERROR,
XZ_DATA_ERROR, XZ_BUF_ERROR
}};
struct z_erofs_decompress_req {{
int partial_decoding;
size_t inputsize;
}};
{extract_function(current["decompressor_lzma.c"], "z_erofs_lzma_error")}
{extract_function(current["decompressor_deflate.c"], "z_erofs_deflate_finish")}
{extract_function(current["decompressor_lzma.c"], "z_erofs_lzma_finish")}
{extract_function(current["decompressor_zstd.c"], "z_erofs_zstd_finish")}
static int failures;
static void
check(const char *name, int actual, int expected)
{{
if (actual != expected) {{
fprintf(stderr, "%s: actual=%d expected=%d\\n", name, actual, expected);
failures++;
}}
}}
int
main(void)
{{
struct z_erofs_decompress_req full = {{ 0, 548 }};
struct z_erofs_decompress_req partial = {{ 1, 548 }};
check("deflate exact", z_erofs_deflate_finish(&full, Z_STREAM_END, 0), 0);
check("deflate tail", z_erofs_deflate_finish(&full, Z_STREAM_END, 8), EINTEGRITY);
check("deflate no end", z_erofs_deflate_finish(&full, 0, 0), EINTEGRITY);
check("deflate partial", z_erofs_deflate_finish(&partial, 0, 8), 0);
check("lzma exact", z_erofs_lzma_finish(&full, XZ_STREAM_END, 548), 0);
check("lzma tail", z_erofs_lzma_finish(&full, XZ_STREAM_END, 540), EINTEGRITY);
check("lzma no end", z_erofs_lzma_finish(&full, XZ_OK, 548), EINTEGRITY);
check("lzma partial", z_erofs_lzma_finish(&partial, XZ_OK, 352), 0);
check("lzma partial error", z_erofs_lzma_finish(&partial, XZ_DATA_ERROR, 352), EINTEGRITY);
check("zstd exact", z_erofs_zstd_finish(&full, 0, 548, 548), 0);
check("zstd tail", z_erofs_zstd_finish(&full, 0, 540, 548), EINTEGRITY);
check("zstd no end", z_erofs_zstd_finish(&full, 1, 548, 548), EINTEGRITY);
check("zstd partial", z_erofs_zstd_finish(&partial, 1, 352, 548), 0);
return failures != 0;
}}
'''
harness_path.write_text(harness, encoding="ascii")
report = {
"baseline": baseline,
"batch": "B27",
"candidate": "P15-083",
"cleanup_and_guards": True,
"codecs": sorted(spec["codecs"]),
"fixture_count": fixture_index["fixture_count"],
"freebsd_positive_errno": True,
"gate": "GO",
"legal_leading_padding_preserved": True,
"linux_tail_semantics_audited": True,
"optional_zstd_abi_preserved": True,
"provider_geom_and_buffer_lifetime_unchanged": True,
"runtime_base": runtime_base,
"status": "PASS",
"test": "TC176-stream-runtime",
"write_set": sorted(all_changed),
}
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
print(json.dumps(report, indent=2, sort_keys=True))
PY
then
pre15_runner_fail 'B27 gate/source/write-set audit failed'
fi
if ! cc -std=c11 -Wall -Wextra -Werror "$PRE15_CASE_TMP/finish-harness.c" \
-o "$PRE15_CASE_TMP/finish-harness" >"$artifacts/finish-harness-build.stdout" \
2>"$artifacts/finish-harness-build.stderr" || \
! "$PRE15_CASE_TMP/finish-harness" >"$artifacts/finish-harness.stdout" \
2>"$artifacts/finish-harness.stderr"; then
pre15_dut_fail 'B27 extracted completion policy harness failed'
fi
sha256sum "$artifacts/P15-083-result.json" \
"$artifacts/P15-083-codec-results.json" \
"$artifacts/B27-fixture-index.json" \
"$artifacts/B27-source-audit.json" >"$artifacts/SHA256SUMS"
fi
pre15_target_reached
if test "${PRE15_MODE:-host}" != qemu; then
printf '%s\n' \
'TC176 B27 host real LZMA/Deflate/Zstd stream-tail subset PASS' \
'Legal padding, trailing garbage, truncation, partial decode, corruption, positive errno, and cleanup PASS' \
'QEMU runtime NOT_RUN in host mode' \
'Full feature suite NOT_RUN'
exit 0
fi
for tool in awk clang file nm scp tar; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B27 QEMU tool: $tool"
done
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
fixture_archive=$PRE15_CASE_TMP/B27-fixtures.tar.gz
module=$PRE15_CASE_TMP/B27-erofs-zstdio1.ko
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
"$PRE15_CASE_TMP/kld-work" 1 >"$artifacts/B27-kld-build.stdout" \
2>"$artifacts/B27-kld-build.stderr"; then
pre15_dut_fail 'B27 cross-target zstdio1 KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/B27-kld-file.txt"
sha256sum "$module" >"$artifacts/B27-kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/B27-kld-nm-u.txt"
tar -C "$first" -czf "$fixture_archive" .
pre15_scp()
{
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
}
if ! pre15_scp "$fixture_archive" /root/B27-fixtures.tar.gz || \
! pre15_scp "$probe" /root/B27-stream-probe.c || \
! pre15_scp "$module" /root/B27-erofs-zstdio1.ko; then
pre15_infra_blocked 'could not transfer B27 module, probe, or fixtures'
fi
if ! pre15_guest_ssh_bounded \
'rm -rf /root/B27-fixtures && mkdir /root/B27-fixtures && tar -xzf /root/B27-fixtures.tar.gz -C /root/B27-fixtures && cc -O2 -Wall -Wextra -Werror -o /root/B27-stream-probe /root/B27-stream-probe.c'; then
pre15_infra_blocked 'could not prepare B27 guest fixtures/probe'
fi
if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
if ! pre15_guest_ssh_bounded kldload /root/B27-erofs-zstdio1.ko \
>"$artifacts/B27-kldload.stdout" 2>"$artifacts/B27-kldload.stderr"; then
if grep -q 'module already loaded or in kernel' \
"$artifacts/B27-kldload.stderr"; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
if grep -Eq 'link_elf|symbol|not defined' "$artifacts/B27-kldload.stderr"; then
pre15_infra_blocked 'guest kernel lacks the planned ZSTDIO provider ABI'
fi
pre15_dut_fail 'B27 exact-source zstdio1 KLD failed to load'
fi
pre15_own_guest_kld erofs 'B27 exact-source KLD'
attach_mount()
{
image=$1
label=$2
B27_MD=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
-f "/root/B27-fixtures/images/$image") || \
pre15_dut_fail "$label md attach failed"
case "$B27_MD" in
md[0-9]*) ;;
*) pre15_runner_fail "unexpected B27 md unit: $B27_MD" ;;
esac
pre15_own_guest_md "$B27_MD" "$label md"
B27_MOUNT=/mnt/pre15-b27-$label
pre15_guest_ssh_bounded mkdir -p "$B27_MOUNT"
pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$B27_MD" "$B27_MOUNT" || \
pre15_dut_fail "$label mount failed"
pre15_own_guest_mount "$B27_MOUNT" "$label mount"
}
reference=/root/B27-fixtures/source/payload.bin
for codec in deflate lzma zstd; do
attach_mount "$codec-valid.erofs" "$codec-valid"
pre15_guest_ssh_bounded cmp "$B27_MOUNT/payload.bin" "$reference" || \
pre15_dut_fail "$codec legal leading-padding read mismatch"
for kind in tail truncated; do
attach_mount "$codec-$kind.erofs" "$codec-$kind"
pre15_guest_ssh_bounded /root/B27-stream-probe errno \
"$B27_MOUNT/payload.bin" 97 || \
pre15_dut_fail "$codec $kind did not return EINTEGRITY"
done
case "$codec" in
deflate) logical_offset=137204; partial_size=3587 ;;
lzma|zstd) logical_offset=0; partial_size=4096 ;;
esac
attach_mount "$codec-corrupt.erofs" "$codec-corrupt"
pre15_guest_ssh_bounded /root/B27-stream-probe range \
"$B27_MOUNT/payload.bin" "$reference" "$logical_offset" \
"$partial_size" || \
pre15_dut_fail "$codec range-before-corruption partial decode failed"
pre15_guest_ssh_bounded /root/B27-stream-probe errno \
"$B27_MOUNT/payload.bin" 97 || \
pre15_dut_fail "$codec full corruption did not return EINTEGRITY"
done
pre15_guest_ssh_bounded dmesg >"$artifacts/B27-dmesg.txt"
printf '%s\n' \
'TC176 B27 QEMU real LZMA/Deflate/Zstd stream-tail subset PASS' \
'valid=3 trailing=3 truncated=3 partial-before-corruption=3 full-corruption=3 errno=97' \
'Full feature suite NOT_RUN'
+667
View File
@@ -0,0 +1,667 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
spec=$fixture_dir/B28-partial.json
generator=$fixture_dir/B28-partial-fixtures.py
probe=$fixture_dir/B28-partial-probe.c
kld_builder=$fixture_dir/B28-build-kld.sh
gate_script=$PRE15_DUT/tests/pre15/gates/P15-086.sh
gate_input=$PRE15_DUT/tests/pre15/gates/P15-086-input.json
gate_result=$PRE15_ROOT/planning/pre15/evidence/20260815T154915Z-G04-G05-P15-086/gate-output/result.json
gate_capabilities=$PRE15_ROOT/planning/pre15/evidence/20260815T154915Z-G04-G05-P15-086/gate-output/authorized-capabilities.json
artifacts=$PRE15_RUN_DIR/artifacts
first=$PRE15_CASE_TMP/fixtures-first
second=$PRE15_CASE_TMP/fixtures-second
action=${1:-runtime}
case "$action" in
runtime|k2) ;;
*) pre15_fail_usage "unknown B28 action: $action" ;;
esac
if test "$action" = k2; then
for tool in awk clang cmp diff file nm sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B28 K2 tool: $tool"
done
pre15_record_fixture b28-case "$PRE15_DUT/tests/pre15/cases/B28-partial.sh"
pre15_record_fixture b28-kld-builder "$kld_builder"
for source in compress.h decompressor.c decompressor_lz4.c \
decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c; do
pre15_record_fixture "b28-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
for config in 0 1; do
module=$PRE15_CASE_TMP/B28-zstdio$config.ko
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
"$PRE15_FREEBSD_SRC" "$module" \
"$PRE15_CASE_TMP/kld-work-$config" "$config" \
>"$artifacts/B28-build-zstdio$config.stdout" \
2>"$artifacts/B28-build-zstdio$config.stderr"; then
pre15_dut_fail "B28 zstdio$config cross-target KLD build failed"
fi
file "$module" >"$artifacts/B28-zstdio$config-file.txt"
sha256sum "$module" >"$artifacts/B28-zstdio$config-sha256.txt"
nm -g "$module" | LC_ALL=C sort \
>"$artifacts/B28-zstdio$config-nm-global.txt"
nm -u "$module" | LC_ALL=C sort \
>"$artifacts/B28-zstdio$config-nm-u.txt"
done
awk '$1 != "U" { print $NF }' "$artifacts/B28-zstdio0-nm-global.txt" \
>"$PRE15_CASE_TMP/B28-zstdio0-global-names.txt"
awk '$1 != "U" { print $NF }' "$artifacts/B28-zstdio1-nm-global.txt" \
>"$PRE15_CASE_TMP/B28-zstdio1-global-names.txt"
if ! diff -u "$PRE15_CASE_TMP/B28-zstdio0-global-names.txt" \
"$PRE15_CASE_TMP/B28-zstdio1-global-names.txt" \
>"$artifacts/B28-config-nm-global.diff"; then
pre15_dut_fail 'B28 K2 global symbols differ by ZSTDIO configuration'
fi
if ! grep -q ' z_erofs_decompress_supports_subextent$' \
"$artifacts/B28-zstdio0-nm-global.txt"; then
pre15_dut_fail 'B28 K2 partial capability symbol is absent'
fi
pre15_target_reached
printf '%s\n' \
'B28 targeted FreeBSD 15 K2 PASS' \
'zstdio0/zstdio1 link, defined-global parity, optional Zstd ABI, and partial capability symbol PASS' \
'KLD and object files removed by owned case cleanup'
exit 0
fi
for tool in cc cmp diff file fsck.erofs git mkfs.erofs nm python3 sha256sum tar timeout; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B28 host tool: $tool"
done
pre15_record_fixture b28-case "$PRE15_DUT/tests/pre15/cases/B28-partial.sh"
pre15_record_fixture b28-spec "$spec"
pre15_record_fixture b28-generator "$generator"
pre15_record_fixture b28-probe "$probe"
pre15_record_fixture b28-kld-builder "$kld_builder"
pre15_record_fixture b28-gate "$gate_script"
pre15_record_fixture b28-gate-input "$gate_input"
pre15_record_fixture b28-gate-result "$gate_result"
pre15_record_fixture b28-gate-capabilities "$gate_capabilities"
for source in compress.h decompressor.c decompressor_lz4.c \
decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c; do
pre15_record_fixture "b28-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
if ! timeout -k 5 120 python3 -B "$generator" --spec "$spec" \
--output "$first" >"$artifacts/generate-first.stdout" \
2>"$artifacts/generate-first.stderr"; then
pre15_runner_fail 'B28 first real EROFS fixture generation failed'
fi
if ! timeout -k 5 120 python3 -B "$generator" --spec "$spec" \
--output "$second" >"$artifacts/generate-second.stdout" \
2>"$artifacts/generate-second.stderr"; then
pre15_runner_fail 'B28 second real EROFS fixture generation failed'
fi
if ! diff -ru "$first" "$second" >"$artifacts/fixture-repeat.diff"; then
pre15_runner_fail 'B28 real EROFS fixtures are not byte reproducible'
fi
cp "$first/fixture-index.json" "$artifacts/B28-fixture-index.json"
if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
pre15_target_reached
else
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$spec" "$gate_result" \
"$gate_capabilities" "$artifacts/B28-source-audit.json" \
"$PRE15_CASE_TMP/decision-harness.c" <<'PY'
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
spec_path = Path(sys.argv[3])
gate_result_path = Path(sys.argv[4])
gate_capabilities_path = Path(sys.argv[5])
report_path = Path(sys.argv[6])
harness_path = Path(sys.argv[7])
spec = json.loads(spec_path.read_text(encoding="ascii"))
baseline = spec["baseline"]
implementation = "7a0d7e4a4047ac6d6130a3be5cb677cb94979fe7"
runtime_base = "d09924362eb29819bd393e1e86602eb38c7608c6"
def fail(message: str) -> None:
raise SystemExit(message)
def sha256_path(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def committed(commit: str, name: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{commit}:repo-pre-15/src/{name}"],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if completed.returncode != 0:
fail(f"cannot read B28 source {commit}:{name}: {completed.stderr.strip()}")
return completed.stdout
def function(source: str, name: str) -> str:
marker = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if marker is None:
fail(f"missing function: {name}")
name_line = source.rfind("\n", 0, marker.start()) + 1
start = source.rfind("\n", 0, name_line - 1) + 1
brace = source.find("{", marker.end())
if brace < 0:
fail(f"missing function body: {name}")
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
fail(f"unterminated function: {name}")
if (
spec.get("schema") != 1
or spec.get("batch") != "B28"
or spec.get("candidate") != "P15-086"
or spec.get("test") != "TC176-stream-runtime"
):
fail("B28 fixture identity changed")
for name, commit in (
("baseline", baseline),
("implementation", implementation),
("runtime base", runtime_base),
):
if subprocess.check_output(
["git", "-C", str(root), "rev-parse", commit], text=True
).strip() != commit:
fail(f"B28 {name} identity changed")
gate = json.loads(gate_result_path.read_text(encoding="ascii"))
capabilities = json.loads(gate_capabilities_path.read_text(encoding="ascii"))
if (
gate.get("status") != "GO"
or gate.get("g04") != "GO"
or gate.get("g05") != "GO"
or gate.get("b28") != "AUTHORIZED"
or gate.get("resolved_base") != "6bf5724619be70f805bbe7d1ba77dd70cdced69f"
):
fail("frozen P15-086 gate no longer authorizes B28")
if capabilities != {
"full_fallback": ["zstd"],
"partial": ["deflate", "lz4", "lzma"],
"persistent_state_added": False,
"schema": 1,
}:
fail("P15-086 per-codec authorization changed")
for name, expected in (
("gate script", spec["gate"]["script_sha256"]),
("gate input", spec["gate"]["input_sha256"]),
("gate result", spec["gate"]["result_sha256"]),
):
path = {
"gate script": dut / "tests/pre15/gates/P15-086.sh",
"gate input": dut / "tests/pre15/gates/P15-086-input.json",
"gate result": gate_result_path,
}[name]
if sha256_path(path) != expected:
fail(f"{name} hash changed")
source_names = (
"compress.h",
"decompressor.c",
"decompressor_lz4.c",
"decompressor_lzma.c",
"decompressor_deflate.c",
"decompressor_zstd.c",
"zdata.c",
)
baseline_sources = {name: committed(baseline, name) for name in source_names}
implemented_sources = {name: committed(implementation, name) for name in source_names}
sources = {
name: (dut / "src" / name).read_text(encoding="utf-8")
for name in source_names
}
if implemented_sources["decompressor_lz4.c"] != baseline_sources["decompressor_lz4.c"]:
fail("B28 changed the already-capable LZ4 backend implementation")
if "bool supports_subextent;" not in implemented_sources["compress.h"]:
fail("B28 descriptor capability is absent")
if implemented_sources["compress.h"].count("z_erofs_decompress_supports_subextent") != 1:
fail("B28 capability prototype count changed")
if "bool supports_subextent;" not in sources["compress.h"]:
fail("current tree lost the B28 descriptor capability")
if sources["compress.h"].count("z_erofs_decompress_supports_subextent") != 1:
fail("current B28 capability prototype count changed")
def read_descriptor_decisions(source_set: dict[str, str]) -> dict[str, bool]:
decisions = {}
for codec in ("lzma", "deflate", "zstd"):
value = re.search(
rf"const struct z_erofs_decompressor z_erofs_{codec}_decomp = \{{.*?\.supports_subextent = ([01]),",
source_set[f"decompressor_{codec}.c"],
re.DOTALL,
)
if value is None:
fail(f"missing {codec} descriptor capability")
decisions[codec] = value.group(1) == "1"
lz4 = re.search(
r"static const struct z_erofs_decompressor z_erofs_lz4_decomp = \{.*?\.supports_subextent = ([01]),",
source_set["decompressor.c"],
re.DOTALL,
)
if lz4 is None:
fail("missing LZ4 descriptor capability")
decisions["lz4"] = lz4.group(1) == "1"
return decisions
implemented_decisions = read_descriptor_decisions(implemented_sources)
if implemented_decisions != {
"lz4": True, "lzma": True, "deflate": True, "zstd": False
}:
fail(f"B28 implementation decisions differ from G04/G05: {implemented_decisions}")
descriptor_decisions = read_descriptor_decisions(sources)
if descriptor_decisions != {
"lz4": True, "lzma": True, "deflate": True, "zstd": False
}:
fail(f"current partial descriptor decisions changed: {descriptor_decisions}")
expected_zstd = committed(runtime_base, "decompressor_zstd.c")
if function(sources["decompressor_zstd.c"], "z_erofs_zstd_finish") != \
function(expected_zstd, "z_erofs_zstd_finish"):
fail("current Zstd completion policy differs from the accepted transform")
unchanged_zdata = (
"z_erofs_extent_cache_match",
"z_erofs_extent_cache_copy",
"z_erofs_extent_cache_publish",
"z_erofs_extent_cache_init",
"z_erofs_extent_cache_fini",
"z_erofs_extent_cache_eligible",
"z_erofs_read_data",
"z_erofs_read_uio",
)
baseline_zdata = baseline_sources["zdata.c"]
for name in unchanged_zdata:
if function(implemented_sources["zdata.c"], name) != function(baseline_zdata, name):
fail(f"B28 changed unrelated zdata function {name}")
implemented_decode_length = function(implemented_sources["zdata.c"], "z_erofs_decode_length")
implemented_read_extent = function(implemented_sources["zdata.c"], "z_erofs_read_extent")
implemented_do_read = function(implemented_sources["zdata.c"], "z_erofs_do_read")
decode_length = function(sources["zdata.c"], "z_erofs_decode_length")
decode_extent = function(sources["zdata.c"], "z_erofs_decode_extent")
do_read = function(sources["zdata.c"], "z_erofs_do_read")
if decode_length != implemented_decode_length:
fail("current tree changed the B28 decode-length transform")
if decode_extent[decode_extent.index("{") :] != implemented_read_extent[
implemented_read_extent.index("{") :
]:
fail("current tree changed B28 provider or decoded-buffer ownership")
for anchor in (
"z_erofs_decompress_supports_subextent(map)",
"*partial = (map->m_flags & EROFS_MAP_PARTIAL_REF) != 0;",
"*decoded_len = end;",
"*partial = true;",
):
if anchor not in implemented_decode_length:
fail(f"B28 decode-length anchor missing: {anchor}")
for anchor in (
"erofs_read_metadata(",
"erofs_read_physical(",
"erofs_put_metabuf(&buf);",
"erofs_brelse(compressed);",
"free(decoded, M_EROFS);",
"*bufp = decoded;",
):
if implemented_read_extent.count(anchor) != function(baseline_zdata, "z_erofs_read_extent").count(anchor):
fail(f"B28 changed provider or cleanup count: {anchor}")
if not (
implemented_do_read.index("z_erofs_extent_cache_copy")
< implemented_do_read.index("z_erofs_decode_length")
< implemented_do_read.index("z_erofs_read_extent")
< implemented_do_read.index("cache_eligible && !partial")
< implemented_do_read.index("z_erofs_extent_cache_publish")
):
fail("B28 cache/decode/publication order changed")
if "partial, &decoded" not in implemented_do_read:
fail("B28 does not pass the selected partial mode to extent decode")
if not (
do_read.index("z_erofs_decode_length")
< do_read.index("cache_eligible = !partial")
< do_read.index("z_erofs_decode_extent")
):
fail("current tree no longer excludes partial decodes from cache admission")
if "partial, &decoded" not in do_read:
fail("current tree does not pass B28 partial mode to extent decode")
changed = set(
subprocess.check_output(
[
"git",
"-C",
str(root),
"diff-tree",
"--no-commit-id",
"--name-only",
"-r",
implementation,
],
text=True,
).splitlines()
)
expected_write_set = {
"repo-pre-15/src/compress.h",
"repo-pre-15/src/decompressor.c",
"repo-pre-15/src/decompressor_deflate.c",
"repo-pre-15/src/decompressor_lzma.c",
"repo-pre-15/src/decompressor_zstd.c",
"repo-pre-15/src/zdata.c",
"repo-pre-15/tests/pre15/cases/B28-partial.sh",
"repo-pre-15/tests/pre15/fixtures/B28-build-kld.sh",
"repo-pre-15/tests/pre15/fixtures/B28-partial-fixtures.py",
"repo-pre-15/tests/pre15/fixtures/B28-partial-probe.c",
"repo-pre-15/tests/pre15/fixtures/B28-partial.json",
}
if changed != expected_write_set:
fail(f"B28 exact write set differs: {sorted(changed ^ expected_write_set)}")
if any(re.search(r"return\s*\(\s*-E[A-Z0-9_]+", source) for source in sources.values()):
fail("negative Linux errno entered B28")
harness = r'''#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#define EINTEGRITY 97
#define EOVERFLOW 84
#define EROFS_MAP_PARTIAL_REF 0x20
typedef uint64_t erofs_off_t;
struct erofs_map_blocks {
uint64_t m_llen;
unsigned int m_flags;
unsigned int m_algorithmformat;
};
static bool z_erofs_decompress_supports_subextent(const struct erofs_map_blocks *map)
{
return map->m_algorithmformat <= 2;
}
''' + decode_length + r'''
static int failures;
static void check(const char *name, struct erofs_map_blocks map, uint64_t off,
size_t want, int expected_error, size_t expected_length, bool expected_partial)
{
size_t length = 0;
bool partial = false;
int error = z_erofs_decode_length(&map, off, want, &length, &partial);
if (error != expected_error || (!error &&
(length != expected_length || partial != expected_partial))) {
fprintf(stderr, "%s error=%d length=%zu partial=%d\n", name,
error, length, partial);
failures++;
}
}
int main(void)
{
check("lz4 prefix", (struct erofs_map_blocks){1038906, 0, 0}, 0,
4096, 0, 4096, true);
check("lzma middle", (struct erofs_map_blocks){151552, 0, 1}, 8192,
4096, 0, 12288, true);
check("deflate cross", (struct erofs_map_blocks){22878, 0, 2}, 3584,
4096, 0, 7680, true);
check("zstd prefix", (struct erofs_map_blocks){151552, 0, 3}, 0,
4096, 0, 151552, false);
check("lzma tail", (struct erofs_map_blocks){151552, 0, 1}, 147456,
4096, 0, 151552, false);
check("zstd partial ref", (struct erofs_map_blocks){151552,
EROFS_MAP_PARTIAL_REF, 3}, 0, 4096, 0, 4096, true);
check("shifted fallback", (struct erofs_map_blocks){65536, 0, 4}, 0,
4096, 0, 65536, false);
check("bad range", (struct erofs_map_blocks){4096, 0, 0}, 4090,
16, EINTEGRITY, 0, false);
return failures != 0;
}
'''
harness_path.write_text(harness, encoding="ascii")
report = {
"baseline": baseline,
"batch": "B28",
"cache_hit_before_decode": True,
"candidate": "P15-086",
"descriptor_decisions": descriptor_decisions,
"frozen_gate_full_fallback": ["zstd"],
"current_full_fallback": ["shifted", "interlaced", "unknown", "zstd"],
"current_partial": ["deflate", "lz4", "lzma"],
"gate": "GO",
"implementation": implementation,
"partial_cache_publication": False,
"partial_reference_preserved": True,
"persistent_state_added": False,
"positive_errno": True,
"provider_and_cleanup_unchanged": True,
"runtime_base": runtime_base,
"status": "PASS",
"test": "TC176-stream-runtime",
"write_set": sorted(changed),
}
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
print(json.dumps(report, indent=2, sort_keys=True))
PY
then
pre15_runner_fail 'B28 gate/source/write-set audit failed'
fi
if ! cc -std=c11 -Wall -Wextra -Werror "$PRE15_CASE_TMP/decision-harness.c" \
-o "$PRE15_CASE_TMP/decision-harness" \
>"$artifacts/decision-harness-build.stdout" \
2>"$artifacts/decision-harness-build.stderr" || \
! "$PRE15_CASE_TMP/decision-harness" \
>"$artifacts/decision-harness.stdout" \
2>"$artifacts/decision-harness.stderr"; then
pre15_dut_fail 'B28 extracted decode-length harness failed'
fi
cp "$gate_result" "$artifacts/P15-086-result.json"
cp "$gate_capabilities" "$artifacts/P15-086-capabilities.json"
sha256sum "$artifacts/P15-086-result.json" \
"$artifacts/P15-086-capabilities.json" \
"$artifacts/B28-fixture-index.json" \
"$artifacts/B28-source-audit.json" >"$artifacts/SHA256SUMS"
fi
pre15_target_reached
if test "${PRE15_MODE:-host}" != qemu; then
printf '%s\n' \
'TC176 B28 host partial-subextent subset PASS' \
'B28 LZ4/LZMA/Deflate partial policy and Zstd full fallback, cache exclusion, arithmetic, real fixtures, and cleanup PASS' \
'QEMU runtime NOT_RUN in host mode' \
'Full feature suite NOT_RUN'
exit 0
fi
for tool in awk clang scp; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B28 QEMU tool: $tool"
done
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
fixture_archive=$PRE15_CASE_TMP/B28-fixtures.tar.gz
module=$PRE15_CASE_TMP/B28-erofs-zstdio1.ko
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
"$PRE15_CASE_TMP/kld-work" 1 >"$artifacts/B28-kld-build.stdout" \
2>"$artifacts/B28-kld-build.stderr"; then
pre15_dut_fail 'B28 cross-target zstdio1 KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/B28-kld-file.txt"
sha256sum "$module" >"$artifacts/B28-kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/B28-kld-nm-u.txt"
tar -C "$first" -czf "$fixture_archive" .
pre15_scp()
{
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
}
pre15_guest_ssh_bounded()
{
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" ssh -n -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-p "$PRE15_QEMU_SSH_PORT" \
"$PRE15_QEMU_SSH_USER@127.0.0.1" "$@"
}
if ! pre15_scp "$fixture_archive" /root/B28-fixtures.tar.gz || \
! pre15_scp "$probe" /root/B28-partial-probe.c || \
! pre15_scp "$module" /root/B28-erofs-zstdio1.ko; then
pre15_infra_blocked 'could not transfer B28 module, probe, or fixtures'
fi
if ! pre15_guest_ssh_bounded \
'rm -rf /root/B28-fixtures && mkdir /root/B28-fixtures && tar -xzf /root/B28-fixtures.tar.gz -C /root/B28-fixtures && cc -O2 -Wall -Wextra -Werror -o /root/B28-partial-probe /root/B28-partial-probe.c'; then
pre15_infra_blocked 'could not prepare B28 guest fixtures/probe'
fi
if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
if ! pre15_guest_ssh_bounded kldload /root/B28-erofs-zstdio1.ko \
>"$artifacts/B28-kldload.stdout" 2>"$artifacts/B28-kldload.stderr"; then
if grep -q 'module already loaded or in kernel' \
"$artifacts/B28-kldload.stderr"; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
if grep -Eq 'link_elf|symbol|not defined' "$artifacts/B28-kldload.stderr"; then
pre15_infra_blocked 'guest kernel lacks the planned ZSTDIO provider ABI'
fi
pre15_dut_fail 'B28 exact-source zstdio1 KLD failed to load'
fi
pre15_own_guest_kld B28-erofs-zstdio1.ko 'B28 exact-source KLD'
attach_mount()
{
image=$1
label=$2
B28_MD=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
-f "/root/B28-fixtures/images/$image") || \
pre15_dut_fail "$label md attach failed"
case "$B28_MD" in
md[0-9]*) ;;
*) pre15_runner_fail "unexpected B28 md unit: $B28_MD" ;;
esac
pre15_own_guest_md "$B28_MD" "$label md"
B28_MOUNT=/mnt/pre15-b28-$label
pre15_guest_ssh_bounded mkdir -p "$B28_MOUNT"
pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$B28_MD" "$B28_MOUNT" || \
pre15_dut_fail "$label mount failed"
pre15_own_guest_mount "$B28_MOUNT" "$label mount"
}
valid_assertions=0
corrupt_range_assertions=0
full_corruption_assertions=0
while IFS='|' read -r codec source logical_offset logical_length decision; do
reference=/root/B28-fixtures/sources/$source/payload.bin
attach_mount "$codec-valid.erofs" "$codec-valid"
for range in '0 4096' '3584 4096' '8192 4096'; do
set -- $range
offset=$((logical_offset + $1))
pre15_guest_ssh_bounded /root/B28-partial-probe range \
"$B28_MOUNT/payload.bin" "$reference" "$offset" "$2" || \
pre15_dut_fail "$codec valid subextent mismatch at $offset"
valid_assertions=$((valid_assertions + 1))
done
tail_offset=$((logical_offset + logical_length - 4096))
pre15_guest_ssh_bounded /root/B28-partial-probe range \
"$B28_MOUNT/payload.bin" "$reference" "$tail_offset" 4096 || \
pre15_dut_fail "$codec valid tail fallback mismatch"
valid_assertions=$((valid_assertions + 1))
attach_mount "$codec-corrupt.erofs" "$codec-corrupt"
if test "$decision" = GO; then
pre15_guest_ssh_bounded /root/B28-partial-probe range \
"$B28_MOUNT/payload.bin" "$reference" "$logical_offset" 4096 || \
pre15_dut_fail "$codec range-before-corruption partial decode failed"
else
pre15_guest_ssh_bounded /root/B28-partial-probe range-errno \
"$B28_MOUNT/payload.bin" "$logical_offset" 4096 97 || \
pre15_dut_fail "$codec did not use exact full fallback"
fi
corrupt_range_assertions=$((corrupt_range_assertions + 1))
pre15_guest_ssh_bounded /root/B28-partial-probe full-errno \
"$B28_MOUNT/payload.bin" 97 || \
pre15_dut_fail "$codec full corruption did not return EINTEGRITY"
full_corruption_assertions=$((full_corruption_assertions + 1))
done <<'EOF'
deflate|stream|114326|22878|GO
lz4|lz4|0|1038906|GO
lzma|stream|0|151552|GO
zstd|stream|0|151552|FULL_FALLBACK
EOF
if test "$valid_assertions" -ne 16 || \
test "$corrupt_range_assertions" -ne 4 || \
test "$full_corruption_assertions" -ne 4; then
pre15_runner_fail "B28 assertion count mismatch: valid=$valid_assertions corrupt-range=$corrupt_range_assertions full-corruption=$full_corruption_assertions"
fi
pre15_guest_ssh_bounded dmesg >"$artifacts/B28-dmesg.txt"
printf '%s\n' \
'TC176 B28 QEMU partial-subextent subset PASS' \
'valid-ranges=16 partial-before-corruption=4 full-corruption=4 errno=97' \
'Full feature suite NOT_RUN'
File diff suppressed because it is too large Load Diff
+448
View File
@@ -0,0 +1,448 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=1b4b8abc249d9bbbb75e19b5259c5f46f91909ef
artifacts=$PRE15_RUN_DIR/artifacts
for tool in git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B30 host tool: $tool"
done
for source in internal.h compress.h decompressor.c decompressor_lz4.c \
decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c zmap.c; do
pre15_record_fixture "b30-$source" "$PRE15_DUT/src/$source"
done
pre15_record_fixture b30-linux-compress "$PRE15_ROOT/src-linux/compress.h"
pre15_record_fixture b30-linux-decompressor "$PRE15_ROOT/src-linux/decompressor.c"
pre15_record_fixture b30-case \
"$PRE15_DUT/tests/pre15/cases/B30-codec-structure.sh"
mkdir -p "$artifacts"
pre15_target_reached
if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \
"$artifacts/B30-codec-structure.json" <<'PY'
from __future__ import annotations
from collections import Counter
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
baseline = sys.argv[3]
report_path = Path(sys.argv[4])
src = dut / "src"
def committed(path: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
raise SystemExit(f"cannot read B30 baseline {path}: {completed.stderr}")
return completed.stdout
def replace_once(source: str, old: str, new: str, label: str) -> str:
count = source.count(old)
if count != 1:
raise SystemExit(f"B30 baseline anchor count for {label}: {count}")
return source.replace(old, new, 1)
expected_changed = {
"repo-pre-15/src/compress.h",
"repo-pre-15/src/decompressor.c",
"repo-pre-15/src/decompressor_deflate.c",
"repo-pre-15/src/decompressor_lzma.c",
"repo-pre-15/src/decompressor_zstd.c",
"repo-pre-15/src/internal.h",
"repo-pre-15/src/zdata.c",
"repo-pre-15/tests/pre15/cases/B30-codec-structure.sh",
}
changed = subprocess.run(
["git", "-C", str(root), "diff", "--name-only", baseline, "--", "repo-pre-15"],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()
changed.extend(
subprocess.run(
[
"git",
"-C",
str(root),
"ls-files",
"--others",
"--exclude-standard",
"--",
"repo-pre-15",
],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()
)
if set(changed) != expected_changed or len(changed) != len(expected_changed):
raise SystemExit(f"B30 repo-pre-15 write set mismatch: {changed!r}")
current = {
path: (src / path).read_text(encoding="utf-8")
for path in (
"internal.h",
"compress.h",
"decompressor.c",
"decompressor_lz4.c",
"decompressor_lzma.c",
"decompressor_deflate.c",
"decompressor_zstd.c",
"zdata.c",
"zmap.c",
)
}
before = {
path: committed(f"src/{path}")
for path in current
}
expected = before["internal.h"]
expected = replace_once(
expected,
"struct erofs_sb_lz4_info {\n"
"\tuint16_t max_distance_pages;\n"
"\tuint16_t max_pclusterblks;\n"
"};\n\n",
"",
"LZ4 state type",
)
expected = replace_once(
expected,
"\tstruct erofs_sb_lz4_info lz4;\n",
"",
"LZ4 state member",
)
if current["internal.h"] != expected:
raise SystemExit("internal.h differs from exact B30 dead-state removal")
expected = before["compress.h"]
expected = replace_once(
expected,
"struct z_erofs_decompressor {\n"
"\tconst char *name;\n"
"\tbool supports_subextent;\n"
"\t/* Callbacks return zero or a positive FreeBSD errno. */\n"
"\tint (*config)(struct erofs_sb_info *, const struct erofs_super_block *,\n"
"\t const void *, size_t);\n"
"\tint (*decompress)(const struct z_erofs_decompress_req *);\n"
"};",
"struct z_erofs_decompressor {\n"
"\t/* Callbacks return zero or a positive FreeBSD errno. */\n"
"\tint (*config)(struct erofs_sb_info *, const struct erofs_super_block *,\n"
"\t const void *, size_t);\n"
"\tint (*decompress)(const struct z_erofs_decompress_req *);\n"
"\tbool supports_subextent;\n"
"\tconst char *name;\n"
"};",
"descriptor fields",
)
if current["compress.h"] != expected:
raise SystemExit("compress.h differs from exact B30 descriptor alignment")
expected = before["decompressor.c"]
for old, new, label in (
("\t\tdistance = le16toh(lz4->max_distance);\n", "", "unused cfg distance"),
("\t\tmax_pclusterblks = 1;\n\t\tsbi->available_compr_algs", "\t\tsbi->available_compr_algs", "legacy dead local store"),
(
"\tsbi->lz4.max_pclusterblks = max_pclusterblks;\n"
"\tsbi->lz4.max_distance_pages = distance != 0 ?\n"
"\t howmany(distance, PAGE_SIZE) + 1 :\n"
"\t howmany(UINT16_MAX, PAGE_SIZE) + 1;\n",
"",
"runtime LZ4 stores",
),
(
"static const struct z_erofs_decompressor z_erofs_shifted_decomp = {\n"
"\t.name = \"shifted\",\n"
"\t.decompress = z_erofs_transform_plain,\n"
"};",
"static const struct z_erofs_decompressor z_erofs_shifted_decomp = {\n"
"\t.decompress = z_erofs_transform_plain,\n"
"\t.name = \"shifted\",\n"
"};",
"shifted descriptor",
),
(
"static const struct z_erofs_decompressor z_erofs_interlaced_decomp = {\n"
"\t.name = \"interlaced\",\n"
"\t.decompress = z_erofs_transform_plain,\n"
"};",
"static const struct z_erofs_decompressor z_erofs_interlaced_decomp = {\n"
"\t.decompress = z_erofs_transform_plain,\n"
"\t.name = \"interlaced\",\n"
"};",
"interlaced descriptor",
),
(
"static const struct z_erofs_decompressor z_erofs_lz4_decomp = {\n"
"\t.name = \"lz4\",\n"
"\t.supports_subextent = 1,\n"
"\t.config = z_erofs_load_lz4_config,\n"
"\t.decompress = z_erofs_lz4_decompress,\n"
"};",
"static const struct z_erofs_decompressor z_erofs_lz4_decomp = {\n"
"\t.config = z_erofs_load_lz4_config,\n"
"\t.decompress = z_erofs_lz4_decompress,\n"
"\t.supports_subextent = 1,\n"
"\t.name = \"lz4\",\n"
"};",
"LZ4 descriptor",
),
(
"static const struct z_erofs_decompressor * const z_erofs_decomp[] = {\n"
"\t[Z_EROFS_COMPRESSION_LZ4] = &z_erofs_lz4_decomp,\n"
"\t[Z_EROFS_COMPRESSION_LZMA] = &z_erofs_lzma_decomp,\n"
"\t[Z_EROFS_COMPRESSION_DEFLATE] = &z_erofs_deflate_decomp,\n"
"\t[Z_EROFS_COMPRESSION_ZSTD] = &z_erofs_zstd_decomp,\n"
"\t[Z_EROFS_COMPRESSION_SHIFTED] = &z_erofs_shifted_decomp,\n"
"\t[Z_EROFS_COMPRESSION_INTERLACED] = &z_erofs_interlaced_decomp,\n"
"};",
"static const struct z_erofs_decompressor * const z_erofs_decomp[] = {\n"
"\t[Z_EROFS_COMPRESSION_SHIFTED] = &z_erofs_shifted_decomp,\n"
"\t[Z_EROFS_COMPRESSION_INTERLACED] = &z_erofs_interlaced_decomp,\n"
"\t[Z_EROFS_COMPRESSION_LZ4] = &z_erofs_lz4_decomp,\n"
"\t[Z_EROFS_COMPRESSION_LZMA] = &z_erofs_lzma_decomp,\n"
"\t[Z_EROFS_COMPRESSION_DEFLATE] = &z_erofs_deflate_decomp,\n"
"\t[Z_EROFS_COMPRESSION_ZSTD] = &z_erofs_zstd_decomp,\n"
"};",
"decompressor table",
),
):
expected = replace_once(expected, old, new, label)
if current["decompressor.c"] != expected:
raise SystemExit("decompressor.c differs from exact B30 structural edits")
for path, name, capability in (
("decompressor_lzma.c", "lzma", "1"),
("decompressor_deflate.c", "deflate", "1"),
("decompressor_zstd.c", "zstd", "0"),
):
expected = before[path]
expected = replace_once(
expected,
f"const struct z_erofs_decompressor z_erofs_{name}_decomp = {{\n"
f"\t.name = \"{name}\",\n"
f"\t.supports_subextent = {capability},\n"
f"\t.config = z_erofs_load_{name}_config,\n"
f"\t.decompress = z_erofs_{name}_decompress,\n"
"};",
f"const struct z_erofs_decompressor z_erofs_{name}_decomp = {{\n"
f"\t.config = z_erofs_load_{name}_config,\n"
f"\t.decompress = z_erofs_{name}_decompress,\n"
f"\t.supports_subextent = {capability},\n"
f"\t.name = \"{name}\",\n"
"};",
f"{name} descriptor",
)
if current[path] != expected:
raise SystemExit(f"{path} differs from exact B30 descriptor reorder")
if current["decompressor_lz4.c"] != before["decompressor_lz4.c"]:
raise SystemExit("B30 changed the independent FreeBSD LZ4 backend")
expected = before["zdata.c"]
if expected.count("z_erofs_read_extent") != 2:
raise SystemExit("B30 zdata baseline rename denominator changed")
expected = expected.replace("z_erofs_read_extent", "z_erofs_decode_extent")
if current["zdata.c"] != expected:
raise SystemExit("zdata.c differs from the exact two-token decode rename")
if current["zmap.c"] != before["zmap.c"]:
raise SystemExit("B30 changed the zmap record reader")
descriptor_markers = [
"int (*config)(",
"int (*decompress)(",
"bool supports_subextent;",
"const char *name;",
]
positions = [current["compress.h"].index(marker) for marker in descriptor_markers]
if positions != sorted(positions):
raise SystemExit("FreeBSD descriptor field order drifted")
def initializer_fields(source: str, object_name: str) -> list[tuple[str, str]]:
match = re.search(
rf"(?:static )?const struct z_erofs_decompressor {object_name} = \{{\n"
rf"(?P<body>.*?)\n\}};",
source,
re.DOTALL,
)
if match is None:
raise SystemExit(f"missing descriptor initializer: {object_name}")
return re.findall(r"^\s*\.([a-z_]+)\s*=\s*([^,]+),$", match.group("body"), re.MULTILINE)
descriptors = {
"shifted": initializer_fields(current["decompressor.c"], "z_erofs_shifted_decomp"),
"interlaced": initializer_fields(current["decompressor.c"], "z_erofs_interlaced_decomp"),
"lz4": initializer_fields(current["decompressor.c"], "z_erofs_lz4_decomp"),
"lzma": initializer_fields(current["decompressor_lzma.c"], "z_erofs_lzma_decomp"),
"deflate": initializer_fields(current["decompressor_deflate.c"], "z_erofs_deflate_decomp"),
"zstd": initializer_fields(current["decompressor_zstd.c"], "z_erofs_zstd_decomp"),
}
expected_descriptors = {
"shifted": [("decompress", "z_erofs_transform_plain"), ("name", '"shifted"')],
"interlaced": [("decompress", "z_erofs_transform_plain"), ("name", '"interlaced"')],
"lz4": [
("config", "z_erofs_load_lz4_config"),
("decompress", "z_erofs_lz4_decompress"),
("supports_subextent", "1"),
("name", '"lz4"'),
],
"lzma": [
("config", "z_erofs_load_lzma_config"),
("decompress", "z_erofs_lzma_decompress"),
("supports_subextent", "1"),
("name", '"lzma"'),
],
"deflate": [
("config", "z_erofs_load_deflate_config"),
("decompress", "z_erofs_deflate_decompress"),
("supports_subextent", "1"),
("name", '"deflate"'),
],
"zstd": [
("config", "z_erofs_load_zstd_config"),
("decompress", "z_erofs_zstd_decompress"),
("supports_subextent", "0"),
("name", '"zstd"'),
],
}
if descriptors != expected_descriptors:
raise SystemExit(f"backend descriptor initializer mismatch: {descriptors!r}")
table_match = re.search(
r"static const struct z_erofs_decompressor \* const z_erofs_decomp\[\] = \{\n"
r"(?P<body>.*?)\n\};",
current["decompressor.c"],
re.DOTALL,
)
if table_match is None:
raise SystemExit("missing FreeBSD decompressor table")
table_order = re.findall(r"\[(Z_EROFS_COMPRESSION_[A-Z0-9_]+)\]", table_match.group("body"))
expected_order = [
"Z_EROFS_COMPRESSION_SHIFTED",
"Z_EROFS_COMPRESSION_INTERLACED",
"Z_EROFS_COMPRESSION_LZ4",
"Z_EROFS_COMPRESSION_LZMA",
"Z_EROFS_COMPRESSION_DEFLATE",
"Z_EROFS_COMPRESSION_ZSTD",
]
if table_order != expected_order:
raise SystemExit(f"FreeBSD decompressor table order mismatch: {table_order!r}")
linux_decompressor = (root / "src-linux/decompressor.c").read_text(encoding="utf-8")
linux_table = linux_decompressor[linux_decompressor.index("z_erofs_decomp[] = {") :]
linux_order = re.findall(r"\[(Z_EROFS_COMPRESSION_[A-Z0-9_]+)\]", linux_table)[:6]
if linux_order != expected_order:
raise SystemExit(f"Linux decompressor table anchor drifted: {linux_order!r}")
callbacks = []
for path in sorted(src.glob("decompressor*.c")):
for line in path.read_text(encoding="utf-8").splitlines():
match = re.match(r"\s*\.(config|decompress)\s*=\s*([^,]+),", line)
if match:
callbacks.append((path.name, match.group(1), match.group(2)))
expected_callbacks = [
("decompressor.c", "decompress", "z_erofs_transform_plain"),
("decompressor.c", "decompress", "z_erofs_transform_plain"),
("decompressor.c", "config", "z_erofs_load_lz4_config"),
("decompressor.c", "decompress", "z_erofs_lz4_decompress"),
("decompressor_deflate.c", "config", "z_erofs_load_deflate_config"),
("decompressor_deflate.c", "decompress", "z_erofs_deflate_decompress"),
("decompressor_lzma.c", "config", "z_erofs_load_lzma_config"),
("decompressor_lzma.c", "decompress", "z_erofs_lzma_decompress"),
("decompressor_zstd.c", "config", "z_erofs_load_zstd_config"),
("decompressor_zstd.c", "decompress", "z_erofs_zstd_decompress"),
]
if Counter(callbacks) != Counter(expected_callbacks) or len(callbacks) != 10:
raise SystemExit(f"G01 decompressor callback multiset changed: {callbacks!r}")
all_source = "\n".join(current.values())
for removed in ("erofs_sb_lz4_info", "max_distance_pages", "sbi->lz4"):
if removed in all_source:
raise SystemExit(f"removed LZ4 runtime state remains: {removed}")
config = current["decompressor.c"][: current["decompressor.c"].index("static int\nz_erofs_transform_plain")]
for marker in (
"uint32_t max_pclusterblks;",
"max_pclusterblks = le16toh(lz4->max_pclusterblks);",
"if (max_pclusterblks == 0)",
"max_pclusterblks = 1;",
"max_pclusterblks >\n\t\t (Z_EROFS_PCLUSTER_MAX_SIZE >> sbi->blkszbits)",
"return (EOPNOTSUPP);",
"distance = le16toh(dsb->u1.lz4_max_distance);",
"if (distance == 0 && !erofs_sb_has_lz4_0padding(sbi))",
):
if marker not in config:
raise SystemExit(f"LZ4 config validation marker missing: {marker!r}")
if current["zdata.c"].count("z_erofs_decode_extent") != 2:
raise SystemExit("zdata decode helper rename denominator changed")
if "z_erofs_read_extent" in current["zdata.c"]:
raise SystemExit("old zdata decode helper name remains")
if current["zmap.c"].count("z_erofs_read_extent") != 4:
raise SystemExit("zmap record reader definition/calls changed")
if "z_erofs_decode_extent" in current["zmap.c"]:
raise SystemExit("zmap record reader was incorrectly renamed")
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", all_source):
raise SystemExit("B30 introduced Linux negative errno")
for path, load_name, decode_name, descriptor_name in (
("decompressor_lzma.c", "z_erofs_load_lzma_config", "z_erofs_lzma_decompress", "z_erofs_lzma_decomp"),
("decompressor_deflate.c", "z_erofs_load_deflate_config", "z_erofs_deflate_decompress", "z_erofs_deflate_decomp"),
("decompressor_zstd.c", "z_erofs_load_zstd_config", "z_erofs_zstd_decompress", "z_erofs_zstd_decomp"),
):
positions = [current[path].index(name) for name in (load_name, decode_name, descriptor_name)]
if positions != sorted(positions):
raise SystemExit(f"backend definition order drifted: {path}")
report = {
"status": "PASS",
"baseline": baseline,
"changed_paths": sorted(changed),
"descriptor_fields": ["config", "decompress", "supports_subextent", "name"],
"descriptor_table_order": table_order,
"callback_field_count": len(callbacks),
"callback_target_multiset": sorted(callbacks),
"removed_runtime_state": ["erofs_sb_lz4_info", "max_distance_pages", "max_pclusterblks store"],
"retained_config_validation": ["max_pclusterblks local", "format cap", "legacy distance branch"],
"rename": {"zdata_decode": 2, "zmap_record_reader": 4},
"preserved": ["B25 typed errno", "B27 trailing policy", "B28 partial fallback ownership"],
}
with report_path.open("x", encoding="ascii") as stream:
json.dump(report, stream, ensure_ascii=True, indent=2, sort_keys=True)
stream.write("\n")
print("B30-descriptors PASS fields=4 backends=6 callbacks=10")
print("B30-write-only PASS dead-state=2 config-local=1 rename=2/4")
PY
then
:
else
pre15_dut_fail 'B30 descriptor, dead-state, or rename oracle failed'
fi
sha256sum "$artifacts/B30-codec-structure.json" > "$artifacts/SHA256SUMS"
printf 'B30 PASS codec structure aligned without behavior changes\n'
+350
View File
@@ -0,0 +1,350 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=643b83c9cea6a99398fa3dd053f2e8f65e108ca0
artifacts=$PRE15_RUN_DIR/artifacts
action=${1:-callgraph}
shift || :
case "$action" in
callgraph|nm-allowlist) ;;
*) pre15_fail_usage "unknown B31 action: $action" ;;
esac
for tool in git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B31 host tool: $tool"
done
pre15_record_fixture b31-data "$PRE15_DUT/src/data.c"
pre15_record_fixture b31-dir "$PRE15_DUT/src/dir.c"
pre15_record_fixture b31-inode "$PRE15_DUT/src/inode.c"
pre15_record_fixture b31-internal "$PRE15_DUT/src/internal.h"
pre15_record_fixture b31-case \
"$PRE15_DUT/tests/pre15/cases/B31-visibility.sh"
mkdir -p "$artifacts"
if ! git -C "$PRE15_ROOT" diff --quiet "$baseline" -- \
repo-pre-15/src/data.c repo-pre-15/src/dir.c \
repo-pre-15/src/inode.c repo-pre-15/src/internal.h; then
pre15_target_reached
pre15_dut_fail 'B31 production files differ from the B30 source baseline'
fi
if test "$action" = callgraph; then
test "$#" -eq 0 || pre15_fail_usage 'B31 callgraph takes no arguments'
for source in zdata.c super.c namei.c erofs_fs.h; do
pre15_record_fixture "b31-$source" "$PRE15_DUT/src/$source"
done
pre15_record_fixture b31-linux-data "$PRE15_ROOT/src-linux/data.c"
pre15_record_fixture b31-linux-internal "$PRE15_ROOT/src-linux/internal.h"
pre15_record_fixture b31-linux-fileio "$PRE15_ROOT/src-linux/fileio.c"
pre15_record_fixture b31-linux-fscache "$PRE15_ROOT/src-linux/fscache.c"
pre15_record_fixture b31-linux-inode "$PRE15_ROOT/src-linux/inode.c"
pre15_record_fixture b31-linux-xattr "$PRE15_ROOT/src-linux/xattr.c"
pre15_record_fixture b31-linux-zmap "$PRE15_ROOT/src-linux/zmap.c"
pre15_record_fixture b31-linux-zdata "$PRE15_ROOT/src-linux/zdata.c"
pre15_target_reached
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" \
"$artifacts/B31-callgraph.json" <<'PY'
from __future__ import annotations
from collections import Counter
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
report_path = Path(sys.argv[3])
src = dut / "src"
symbols = {
"erofs_map_dev": "data.c",
"erofs_map_blocks": "data.c",
"erofs_dirent_namelen": "dir.c",
"erofs_iloc": "inode.c",
}
expected_occurrences = {
"erofs_map_dev": {"data.c": 2},
"erofs_map_blocks": {"data.c": 3, "super.c": 1, "zdata.c": 1},
"erofs_dirent_namelen": {"dir.c": 3, "namei.c": 1},
"erofs_iloc": {"inode.c": 3},
}
expected_cross_tu = {
"erofs_map_dev": [],
"erofs_map_blocks": ["super.c", "zdata.c"],
"erofs_dirent_namelen": ["namei.c"],
"erofs_iloc": [],
}
introduced_by = {
("erofs_map_blocks", "super.c"): "55c609db",
("erofs_map_blocks", "zdata.c"): "55c609db",
("erofs_dirent_namelen", "namei.c"): "c7508502",
}
def strip_noncode(source: str) -> str:
output = list(source)
state = "code"
index = 0
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
output[index] = output[index + 1] = " "
state = "block"
index += 2
continue
if char == "/" and following == "/":
output[index] = output[index + 1] = " "
state = "line"
index += 2
continue
if char == '"':
output[index] = " "
state = "string"
elif char == "'":
output[index] = " "
state = "character"
elif state == "block":
if char == "*" and following == "/":
output[index] = output[index + 1] = " "
state = "code"
index += 2
continue
if char != "\n":
output[index] = " "
elif state == "line":
if char == "\n":
state = "code"
else:
output[index] = " "
else:
if char == "\\" and following:
output[index] = output[index + 1] = " "
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
if char != "\n":
output[index] = " "
index += 1
if state == "block":
raise SystemExit("unterminated block comment")
return "".join(output)
sources = {
path.name: strip_noncode(path.read_text(encoding="utf-8"))
for path in sorted(src.glob("*.c"))
}
locations: dict[str, list[dict[str, object]]] = {}
cross_tu: dict[str, list[str]] = {}
history: dict[str, str] = {}
for symbol, owner in symbols.items():
pattern = re.compile(r"\b" + re.escape(symbol) + r"\s*\(")
sites = []
counts = Counter()
for filename, source in sources.items():
for match in pattern.finditer(source):
line = source.count("\n", 0, match.start()) + 1
sites.append({"file": filename, "line": line})
counts[filename] += 1
actual_counts = dict(sorted(counts.items()))
if actual_counts != expected_occurrences[symbol]:
raise SystemExit(
f"{symbol} call-token inventory changed: {actual_counts!r}"
)
consumers = sorted(filename for filename in counts if filename != owner)
if consumers != expected_cross_tu[symbol]:
raise SystemExit(f"{symbol} cross-TU consumers changed: {consumers!r}")
locations[symbol] = sites
cross_tu[symbol] = consumers
for filename in consumers:
for site in sites:
if site["file"] != filename:
continue
blamed = subprocess.run(
[
"git",
"-C",
str(root),
"blame",
"--line-porcelain",
f"-L{site['line']},{site['line']}",
"--",
f"repo-pre-15/src/{filename}",
],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()[0].split()[0]
expected = introduced_by[(symbol, filename)]
if not blamed.startswith(expected):
raise SystemExit(
f"{symbol} {filename} introducer changed: {blamed}"
)
history[f"{symbol}:{filename}"] = blamed
internal = (src / "internal.h").read_text(encoding="utf-8")
for symbol in symbols:
prototype_count = len(
re.findall(r"\b" + re.escape(symbol) + r"\s*\(", internal)
)
if prototype_count != 1:
raise SystemExit(f"{symbol} prototype count changed: {prototype_count}")
all_source = "\n".join(
path.read_text(encoding="utf-8") for path in sorted(src.glob("*.[ch]"))
)
mtime_helper_invocations = internal.count(
"EROFS_FEATURE_FUNCS(mtime, compat, COMPAT_MTIME)"
)
mtime_explicit_references = len(re.findall(r"\berofs_sb_has_mtime\b", all_source))
if mtime_helper_invocations != 1 or mtime_explicit_references != 0:
raise SystemExit(
"mtime helper inventory changed: "
f"generator={mtime_helper_invocations} explicit={mtime_explicit_references}"
)
erofs_fs = (src / "erofs_fs.h").read_text(encoding="utf-8")
inode = (src / "inode.c").read_text(encoding="utf-8")
super_source = (src / "super.c").read_text(encoding="utf-8")
timestamp_anchors = {
"ondisk_constant": "#define EROFS_FEATURE_COMPAT_MTIME" in erofs_fs,
"compact_checked_add": "__builtin_add_overflow(sbi->epoch," in inode,
"extended_timestamp": "(int64_t)le64toh(die->i_mtime)" in inode,
"epoch_decode": "sbi->epoch = (int64_t)le64toh(dsb->epoch);" in super_source,
"fixed_nsec_decode": "sbi->fixed_nsec = le32toh(dsb->fixed_nsec);" in super_source,
}
if not all(timestamp_anchors.values()):
raise SystemExit(f"timestamp path changed: {timestamp_anchors!r}")
linux_src = root / "src-linux"
linux_text = {
path.name: strip_noncode(path.read_text(encoding="utf-8"))
for path in (
linux_src / "data.c",
linux_src / "fileio.c",
linux_src / "fscache.c",
linux_src / "inode.c",
linux_src / "xattr.c",
linux_src / "zmap.c",
linux_src / "zdata.c",
linux_src / "internal.h",
)
}
linux_internal = linux_text["internal.h"]
if not re.search(r"\bint\s+erofs_map_dev\s*\(", linux_internal):
raise SystemExit("Linux erofs_map_dev external prototype is absent")
if not re.search(r"\bint\s+erofs_map_blocks\s*\(", linux_internal):
raise SystemExit("Linux erofs_map_blocks external prototype is absent")
if not re.search(
r"static\s+inline\s+erofs_off_t\s+erofs_iloc\s*\(", linux_internal
):
raise SystemExit("Linux erofs_iloc is no longer static inline")
if re.search(r"\berofs_dirent_namelen\s*\(", "\n".join(linux_text.values())):
raise SystemExit("unexpected Linux erofs_dirent_namelen symbol")
linux_consumers = {}
for symbol in ("erofs_map_dev", "erofs_map_blocks", "erofs_iloc"):
pattern = re.compile(r"\b" + re.escape(symbol) + r"\s*\(")
linux_consumers[symbol] = sorted(
filename for filename, source in linux_text.items() if pattern.search(source)
)
report = {
"decision": "STOP-NO-SOURCE",
"freebsd_call_tokens": locations,
"freebsd_cross_tu_consumers": cross_tu,
"introduced_by": history,
"linux_linkage": {
"erofs_map_dev": "external",
"erofs_map_blocks": "external",
"erofs_dirent_namelen": "absent",
"erofs_iloc": "static inline",
},
"linux_consumer_files_in_probe": linux_consumers,
"mtime_helper": {
"generator_invocations": mtime_helper_invocations,
"explicit_references": mtime_explicit_references,
},
"timestamp_anchors": timestamp_anchors,
}
report_path.write_text(
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii"
)
PY
then
pre15_dut_fail 'B31 cross-TU visibility audit failed'
fi
printf '%s\n' \
'B31 callgraph audit reached mandatory STOP' \
'erofs_map_blocks: cross-TU consumers super.c,zdata.c introduced by B07c' \
'erofs_dirent_namelen: cross-TU consumer namei.c introduced by B10' \
'erofs_map_dev and erofs_iloc remain same-TU only; atomic B31 source edit is blocked' \
'mtime helper remains generated with zero explicit references; ondisk constant and timestamp paths remain'
pre15_gate_stop \
'B31 blocked: B07c/B10 introduced cross-TU consumers for listed symbols'
fi
test "$#" -eq 2 || \
pre15_fail_usage 'B31 nm-allowlist requires zstdio0 and zstdio1 modules'
for tool in awk diff nm; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B31 nm tool: $tool"
done
module0=$1
module1=$2
pre15_record_fixture b31-zstdio0-module "$module0"
pre15_record_fixture b31-zstdio1-module "$module1"
pre15_target_reached
for config in 0 1; do
eval module=\$module$config
nm -g --defined-only "$module" | awk '{ print $NF }' | LC_ALL=C sort \
>"$artifacts/B31-zstdio$config-global-names.txt"
nm -u "$module" | awk '{ print $NF }' | LC_ALL=C sort \
>"$artifacts/B31-zstdio$config-undefined-names.txt"
for symbol in erofs_map_dev erofs_map_blocks erofs_dirent_namelen erofs_iloc; do
if ! awk -v symbol="$symbol" '$0 == symbol { found = 1 } END { exit !found }' \
"$artifacts/B31-zstdio$config-global-names.txt"; then
pre15_dut_fail "B31 zstdio$config lost required global $symbol"
fi
done
if awk '$0 == "erofs_sb_has_mtime" { found = 1 } END { exit !found }' \
"$artifacts/B31-zstdio$config-global-names.txt" || \
awk '$0 == "erofs_sb_has_mtime" { found = 1 } END { exit !found }' \
"$artifacts/B31-zstdio$config-undefined-names.txt"; then
pre15_dut_fail "B31 zstdio$config unexpectedly emits erofs_sb_has_mtime"
fi
done
if ! diff -u "$artifacts/B31-zstdio0-global-names.txt" \
"$artifacts/B31-zstdio1-global-names.txt" \
>"$artifacts/B31-config-global.diff"; then
pre15_dut_fail 'B31 defined globals differ by ZSTDIO configuration'
fi
printf '%s\n' \
'B31 nm allowlist PASS' \
'global delta from B30 source baseline: zero' \
'four listed symbols remain global in both configurations after mandatory STOP' \
'erofs_sb_has_mtime is not emitted or undefined in either configuration'
+334
View File
@@ -0,0 +1,334 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
artifacts=$PRE15_RUN_DIR/artifacts
action=${1:-callgraph}
shift || :
case "$action" in
callgraph|nm-allowlist) ;;
*) pre15_fail_usage "unknown B31r action: $action" ;;
esac
for tool in git nm python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B31r host tool: $tool"
done
for source in data.c dir.c inode.c internal.h namei.c super.c zdata.c zmap.c; do
pre15_record_fixture "b31r-$source" "$PRE15_DUT/src/$source"
done
pre15_record_fixture b31r-case \
"$PRE15_DUT/tests/pre15/cases/B31r-visibility.sh"
mkdir -p "$artifacts"
if test "$action" = callgraph; then
test "$#" -eq 0 || pre15_fail_usage 'B31r callgraph takes no arguments'
pre15_target_reached
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" \
"$artifacts/B31r-callgraph.json" <<'PY'
from __future__ import annotations
from collections import Counter
import hashlib
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
report_path = Path(sys.argv[3])
src = dut / "src"
symbols = {
"erofs_map_dev": "data.c",
"erofs_map_blocks": "data.c",
"erofs_dirent_namelen": "dir.c",
"erofs_iloc": "inode.c",
}
expected_occurrences = {
"erofs_map_dev": {"data.c": 2},
"erofs_map_blocks": {"data.c": 3, "super.c": 1, "zdata.c": 1},
"erofs_dirent_namelen": {"dir.c": 3, "namei.c": 1},
"erofs_iloc": {"inode.c": 3},
}
expected_cross_tu = {
"erofs_map_dev": [],
"erofs_map_blocks": ["super.c", "zdata.c"],
"erofs_dirent_namelen": ["namei.c"],
"erofs_iloc": [],
}
introduced_by = {
("erofs_map_blocks", "super.c"): "55c609db",
("erofs_map_blocks", "zdata.c"): "55c609db",
("erofs_dirent_namelen", "namei.c"): "c7508502",
}
def strip_noncode(source: str) -> str:
output = list(source)
state = "code"
index = 0
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
output[index] = output[index + 1] = " "
state = "block"
index += 2
continue
if char == "/" and following == "/":
output[index] = output[index + 1] = " "
state = "line"
index += 2
continue
if char == '"':
output[index] = " "
state = "string"
elif char == "'":
output[index] = " "
state = "character"
elif state == "block":
if char == "*" and following == "/":
output[index] = output[index + 1] = " "
state = "code"
index += 2
continue
if char != "\n":
output[index] = " "
elif state == "line":
if char == "\n":
state = "code"
else:
output[index] = " "
else:
if char == "\\" and following:
output[index] = output[index + 1] = " "
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
if char != "\n":
output[index] = " "
index += 1
if state == "block":
raise SystemExit("unterminated block comment")
return "".join(output)
sources = {
path.name: strip_noncode(path.read_text(encoding="utf-8"))
for path in sorted(src.glob("*.c"))
}
locations: dict[str, list[dict[str, object]]] = {}
cross_tu: dict[str, list[str]] = {}
history: dict[str, str] = {}
for symbol, owner in symbols.items():
pattern = re.compile(r"\b" + re.escape(symbol) + r"\s*\(")
sites = []
counts = Counter()
for filename, source in sources.items():
for match in pattern.finditer(source):
line = source.count("\n", 0, match.start()) + 1
sites.append({"file": filename, "line": line})
counts[filename] += 1
actual_counts = dict(sorted(counts.items()))
if actual_counts != expected_occurrences[symbol]:
raise SystemExit(
f"{symbol} call-token inventory changed: {actual_counts!r}"
)
consumers = sorted(filename for filename in counts if filename != owner)
if consumers != expected_cross_tu[symbol]:
raise SystemExit(f"{symbol} cross-TU consumers changed: {consumers!r}")
locations[symbol] = sites
cross_tu[symbol] = consumers
for filename in consumers:
for site in sites:
if site["file"] != filename:
continue
blamed = subprocess.run(
[
"git",
"-C",
str(root),
"blame",
"--line-porcelain",
f"-L{site['line']},{site['line']}",
"--",
f"repo-pre-15/src/{filename}",
],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()[0].split()[0]
expected = introduced_by[(symbol, filename)]
if not blamed.startswith(expected):
raise SystemExit(
f"{symbol} {filename} introducer changed: {blamed}"
)
history[f"{symbol}:{filename}"] = blamed
internal = (src / "internal.h").read_text(encoding="utf-8")
for symbol in symbols:
prototype_count = len(
re.findall(r"\b" + re.escape(symbol) + r"\s*\(", internal)
)
expected = 1 if symbol in {"erofs_map_blocks", "erofs_dirent_namelen"} else 0
if prototype_count != expected:
raise SystemExit(
f"{symbol} prototype count changed: {prototype_count}, expected {expected}"
)
data = (src / "data.c").read_text(encoding="utf-8")
inode = (src / "inode.c").read_text(encoding="utf-8")
if not re.search(r"\bstatic\s+int\s+erofs_map_dev\s*\(", data):
raise SystemExit("erofs_map_dev is not static in data.c")
if not re.search(r"\bstatic\s+erofs_off_t\s+erofs_iloc\s*\(", inode):
raise SystemExit("erofs_iloc is not static in inode.c")
all_source = "\n".join(
path.read_text(encoding="utf-8") for path in sorted(src.glob("*.[ch]"))
)
mtime_helper_invocations = all_source.count(
"EROFS_FEATURE_FUNCS(mtime, compat, COMPAT_MTIME)"
)
mtime_explicit_references = len(re.findall(r"\berofs_sb_has_mtime\b", all_source))
if mtime_helper_invocations != 0 or mtime_explicit_references != 0:
raise SystemExit(
"mtime helper inventory changed: "
f"generator={mtime_helper_invocations} explicit={mtime_explicit_references}"
)
erofs_fs = (src / "erofs_fs.h").read_text(encoding="utf-8")
super_source = (src / "super.c").read_text(encoding="utf-8")
timestamp_anchors = {
"ondisk_constant": "#define EROFS_FEATURE_COMPAT_MTIME" in erofs_fs,
"compact_checked_add": "__builtin_add_overflow(sbi->epoch," in inode,
"extended_timestamp": "(int64_t)le64toh(die->i_mtime)" in inode,
"epoch_decode": "sbi->epoch = (int64_t)le64toh(dsb->epoch);" in super_source,
"fixed_nsec_decode": "sbi->fixed_nsec = le32toh(dsb->fixed_nsec);" in super_source,
}
if not all(timestamp_anchors.values()):
raise SystemExit(f"timestamp path changed: {timestamp_anchors!r}")
source_hashes = {}
for path in sorted(src.glob("*.[ch]")):
source_hashes[path.relative_to(dut).as_posix()] = hashlib.sha256(
path.read_bytes()
).hexdigest()
report = {
"decision": "PASS",
"freebsd_call_tokens": locations,
"freebsd_cross_tu_consumers": cross_tu,
"introduced_by": history,
"linkage": {
"erofs_map_dev": "static",
"erofs_map_blocks": "external",
"erofs_dirent_namelen": "external",
"erofs_iloc": "static",
},
"internal_prototypes": {
symbol: len(re.findall(r"\b" + re.escape(symbol) + r"\s*\(", internal))
for symbol in symbols
},
"mtime_helper": {
"generator_invocations": mtime_helper_invocations,
"explicit_references": mtime_explicit_references,
},
"timestamp_anchors": timestamp_anchors,
"source_hashes": source_hashes,
}
report_path.write_text(
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii"
)
(report_path.parent / "B31r-source-sha256.txt").write_text(
"".join(f"{digest} {path}\n" for path, digest in source_hashes.items()),
encoding="ascii",
)
PY
then
pre15_dut_fail 'B31r independent callgraph audit failed'
fi
printf '%s\n' \
'B31r independent cross-TU callgraph PASS' \
'erofs_map_dev and erofs_iloc are same-TU and static' \
'erofs_map_blocks and erofs_dirent_namelen retain external linkage' \
'erofs_sb_has_mtime helper is absent; timestamp anchors remain' \
'source hashes and exact consumers recorded in B31r-callgraph.json'
exit 0
fi
test "$#" -eq 2 || \
pre15_fail_usage 'B31r nm-allowlist requires zstdio0 and zstdio1 modules'
for module in "$@"; do
pre15_record_fixture "b31r-module-$module" "$module"
done
pre15_target_reached
for config in 0 1; do
case "$config" in
0) module=$1 ;;
1) module=$2 ;;
esac
nm -an "$module" >"$artifacts/B31r-zstdio$config-nm-an.txt"
awk '$NF ~ /^(erofs_map_dev|erofs_map_blocks|erofs_dirent_namelen|erofs_iloc|erofs_sb_has_mtime)$/' \
"$artifacts/B31r-zstdio$config-nm-an.txt" \
>"$artifacts/B31r-zstdio$config-target-bindings.txt"
nm -g --defined-only "$module" | awk '{ print $NF }' | LC_ALL=C sort \
>"$artifacts/B31r-zstdio$config-global-names.txt"
nm -u "$module" | awk '{ print $NF }' | LC_ALL=C sort \
>"$artifacts/B31r-zstdio$config-undefined-names.txt"
for symbol in erofs_map_blocks erofs_dirent_namelen; do
if ! awk -v symbol="$symbol" '$0 == symbol { found = 1 } END { exit !found }' \
"$artifacts/B31r-zstdio$config-global-names.txt"; then
pre15_dut_fail "B31r zstdio$config lost required global $symbol"
fi
done
for symbol in erofs_map_dev erofs_iloc erofs_sb_has_mtime; do
if awk -v symbol="$symbol" '$0 == symbol { found = 1 } END { exit !found }' \
"$artifacts/B31r-zstdio$config-global-names.txt" || \
awk -v symbol="$symbol" '$0 == symbol { found = 1 } END { exit !found }' \
"$artifacts/B31r-zstdio$config-undefined-names.txt"; then
pre15_dut_fail "B31r zstdio$config unexpectedly exposes $symbol"
fi
done
for symbol in erofs_map_dev erofs_iloc; do
if ! awk -v symbol="$symbol" \
'$NF == symbol && $(NF - 1) !~ /^[a-z]$/ { bad = 1 } END { exit bad }' \
"$artifacts/B31r-zstdio$config-nm-an.txt"; then
pre15_dut_fail "B31r zstdio$config has non-local binding for $symbol"
fi
done
if awk '$NF == "erofs_sb_has_mtime" { found = 1 } END { exit !found }' \
"$artifacts/B31r-zstdio$config-nm-an.txt"; then
pre15_dut_fail "B31r zstdio$config emits erofs_sb_has_mtime"
fi
done
if ! diff -u "$artifacts/B31r-zstdio0-global-names.txt" \
"$artifacts/B31r-zstdio1-global-names.txt" \
>"$artifacts/B31r-config-global.diff"; then
pre15_dut_fail 'B31r defined globals differ by ZSTDIO configuration'
fi
printf '%s\n' \
'B31r nm allowlist PASS' \
'cross-TU symbols remain global in both configurations' \
'same-TU symbols are absent or locally bound and never global/undefined' \
'erofs_sb_has_mtime is absent from all symbol bindings' \
'zstdio0/zstdio1 defined-global sets match'
+601
View File
@@ -0,0 +1,601 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
spec=$fixture_dir/B32-cache-state.json
oracle=$fixture_dir/B32-cache-oracle.c
generator=$fixture_dir/B32-cache-generate.py
probe=$fixture_dir/B32-cache-probe.c
qemu_runner=$fixture_dir/B32-qemu-run.sh
kld_builder=$fixture_dir/B28-build-kld.sh
artifacts=$PRE15_RUN_DIR/artifacts
scenario=${PRE15_B32_SCENARIO:-TC168-cache-inflight}
case "$scenario" in
TC168-cache-inflight|TC184-vnode-races) ;;
*) pre15_fail_usage "unknown B32 scenario: $scenario" ;;
esac
for tool in cc git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B32 host tool: $tool"
done
for fixture in "$spec" "$oracle" "$generator" "$probe" "$qemu_runner" \
"$kld_builder"; do
pre15_record_fixture "b32-$(basename "$fixture")" "$fixture"
done
for source in internal.h decompressor.c zdata.c zmap.c; do
pre15_record_fixture "b32-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
pre15_target_reached
else
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$spec" \
"$artifacts/B32-source-audit.json" <<'PY'
from __future__ import annotations
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
spec = json.loads(Path(sys.argv[3]).read_text(encoding="ascii"))
report_path = Path(sys.argv[4])
baseline = spec["baseline"]
implementation = "c6a502184da6e973b1f0fa2ff8c0290c041e7092"
current_owners = {
"internal.h": "aa20623132b6821c6ad4651b263fc0ac0f7a14c8",
"decompressor.c": implementation,
"zdata.c": "bc56f830918b76029871b60cca2e53992de70a2e",
"zmap.c": "d09924362eb29819bd393e1e86602eb38c7608c6",
}
def fail(message: str) -> None:
raise SystemExit(message)
def committed(commit: str, path: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{commit}:repo-pre-15/{path}"],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if completed.returncode != 0:
fail(f"cannot read B32 source {commit}:{path}: {completed.stderr.strip()}")
return completed.stdout
def function(source: str, name: str) -> str:
match = re.search(rf"\n(?:static\s+)?[^\n]+\n{name}\([^{{]+\n\{{", source)
if match is None:
fail(f"cannot locate function {name}")
start = match.start() + 1
brace = source.find("{", match.start())
depth = 0
for index in range(brace, len(source)):
if source[index] == "{":
depth += 1
elif source[index] == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
fail(f"unterminated function {name}")
expected_changed = {
"repo-pre-15/src/decompressor.c",
"repo-pre-15/src/internal.h",
"repo-pre-15/src/zdata.c",
"repo-pre-15/src/zmap.c",
"repo-pre-15/tests/pre15/cases/B32-cache-state.sh",
"repo-pre-15/tests/pre15/fixtures/B32-cache-generate.py",
"repo-pre-15/tests/pre15/fixtures/B32-cache-oracle.c",
"repo-pre-15/tests/pre15/fixtures/B32-cache-probe.c",
"repo-pre-15/tests/pre15/fixtures/B32-qemu-run.sh",
"repo-pre-15/tests/pre15/fixtures/B32-cache-state.json",
}
changed = subprocess.run(
[
"git",
"-C",
str(root),
"diff-tree",
"--no-commit-id",
"--name-only",
"-r",
implementation,
],
check=True,
stdout=subprocess.PIPE,
text=True,
).stdout.splitlines()
if set(changed) != expected_changed or len(changed) != len(expected_changed):
fail(f"B32 implementation write set mismatch: {changed!r}")
for name, commit in (("baseline", baseline), ("implementation", implementation)):
if subprocess.check_output(
["git", "-C", str(root), "rev-parse", commit], text=True
).strip() != commit:
fail(f"B32 {name} identity changed")
sources = {
name: (dut / "src" / name).read_text(encoding="utf-8")
for name in ("internal.h", "decompressor.c", "zdata.c", "zmap.c")
}
implemented_sources = {
name: committed(implementation, f"src/{name}") for name in sources
}
for name, owner in current_owners.items():
if sources[name] != committed(owner, f"src/{name}"):
fail(f"current B32 source differs from accepted owner {owner}: {name}")
required_key_tokens = {
"nid": "cache->nid == vi->nid",
"decoded_size": "cache->decoded_size == decoded_size",
"m_pa": "cache->map.m_pa == map->m_pa",
"m_la": "cache->map.m_la == map->m_la",
"m_plen": "cache->map.m_plen == map->m_plen",
"m_llen": "cache->map.m_llen == map->m_llen",
"m_deviceid": "cache->map.m_deviceid == map->m_deviceid",
"m_algorithmformat": "cache->map.m_algorithmformat == map->m_algorithmformat",
"m_flags": "cache->map.m_flags == map->m_flags",
}
if list(required_key_tokens) != spec["key_fields"]:
fail("B32 key fixture field order changed")
def audit_b32_sources(label: str, source_set: dict[str, str]) -> None:
internal = source_set["internal.h"]
zdata = source_set["zdata.c"]
decompressor = source_set["decompressor.c"]
zmap = source_set["zmap.c"]
map_match = re.search(
r"struct erofs_map_blocks \{(?P<body>.*?)\n\};", internal, re.S
)
cache_match = re.search(
r"struct erofs_zextent_cache \{(?P<body>.*?)\n\};", internal, re.S
)
if map_match is None or cache_match is None:
fail(f"{label} canonical map/cache structures are absent")
if map_match.start() > cache_match.start():
fail(f"{label} canonical map is not defined before embedded cache use")
map_body = map_match.group("body")
cache_body = cache_match.group("body")
if "uint8_t m_algorithmformat;" not in map_body:
fail(f"{label} m_algorithmformat is not an unsigned narrow type")
if "struct erofs_map_blocks map;" not in cache_body:
fail(f"{label} cache does not embed the canonical map")
for duplicate in required_key_tokens:
if duplicate in ("nid", "decoded_size"):
continue
if re.search(
rf"\b{duplicate}\b",
cache_body.replace("struct erofs_map_blocks map;", ""),
):
fail(f"{label} cache duplicates canonical map field {duplicate}")
for field in ("decoded_size", "waiters", "error", "state", "cv", "closing"):
if not re.search(rf"\b{field}\b", cache_body):
fail(f"{label} cache state is missing {field}")
match_body = function(zdata, "z_erofs_extent_cache_match")
for field, token in required_key_tokens.items():
if match_body.count(token) != 1:
fail(f"{label} cache key does not compare {field} exactly once")
for token in (
"cv_wait(&cache->cv, &sbi->z_extent_cache_lock)",
"cv_broadcast(&cache->cv)",
"cache->state = EROFS_ZCACHE_INFLIGHT",
"cache->state = EROFS_ZCACHE_READY",
"cache->state = EROFS_ZCACHE_FAILED",
"cache->waiters != 0",
"cache->error = error",
"cache->closing = true",
):
if token not in zdata:
fail(f"{label} cache state-machine token is absent: {token}")
if "cache->state == EROFS_ZCACHE_INFLIGHT || cache->waiters != 0" not in zdata:
fail(f"{label} eviction does not protect registered waiter generations")
if "z_erofs_extent_cache_eligible(sbi, vi, &map, decoded_len)" not in zdata:
fail(f"{label} cache admission is not based on decoded size")
if "Z_EROFS_CACHE_BYPASS" not in zdata or "free(decoded, M_EROFS);" not in zdata:
fail(f"{label} no-cache fallback is absent")
if (
"uint8_t algorithm;" not in decompressor
or "(unsigned char)map->m_algorithmformat" in decompressor
):
fail(f"{label} decompressor algorithm boundary is not unsigned narrow")
for token in (
"(uint8_t)(map->m_plen >>",
"(uint8_t)(fmt - 1)",
"(uint8_t)(h->h_algorithmtype & 15)",
"(uint8_t)(h->h_algorithmtype >> 4)",
):
if token not in zmap:
fail(f"{label} explicit on-disk conversion is absent: {token}")
for forbidden in ("bitlock", "wait_on_bit", "workqueue", "work_struct", "folio"):
if forbidden in "\n".join(source_set.values()):
fail(f"{label} introduced Linux-only primitive: {forbidden}")
audit_b32_sources("implementation", implemented_sources)
audit_b32_sources("current", sources)
baseline_zdata = committed(baseline, "src/zdata.c")
for preserved in ("z_erofs_decode_length", "z_erofs_decode_extent"):
if function(implemented_sources["zdata.c"], preserved) != function(
baseline_zdata, preserved
):
fail(f"B28 {preserved} changed outside B32 cache state")
for untouched in (
"compress.h",
"decompressor_lz4.c",
"decompressor_lzma.c",
"decompressor_deflate.c",
"decompressor_zstd.c",
):
if committed(implementation, f"src/{untouched}") != committed(
baseline, f"src/{untouched}"
):
fail(f"B32 changed B25/B27/B28 source outside its write set: {untouched}")
current = (dut / "src" / untouched).read_text(encoding="utf-8")
owner = current_owners["zmap.c"] if untouched == "decompressor_zstd.c" else baseline
if current != committed(owner, f"src/{untouched}"):
fail(f"current preserved codec source differs from accepted owner: {untouched}")
report = {
"algorithm_type": "uint8_t",
"baseline": baseline,
"batch": "B32",
"cache_map_embedded": True,
"current_owners": current_owners,
"decoded_size_admission": True,
"implementation": implementation,
"key_fields": spec["key_fields"],
"lock": "z_extent_cache_lock",
"no_cache_fallback": True,
"preserved": ["B25-positive-errno", "B27-trailing", "B28-partial-fallback"],
"states": spec["states"],
"write_set": sorted(changed),
}
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
print(json.dumps(report, indent=2, sort_keys=True))
PY
then
pre15_runner_fail 'B32 source, lock, key, or preservation audit failed'
fi
if ! cc -std=c11 -Wall -Wextra -Werror -pthread "$oracle" \
-o "$PRE15_CASE_TMP/B32-cache-oracle" \
>"$artifacts/oracle-build.stdout" 2>"$artifacts/oracle-build.stderr"; then
pre15_runner_fail 'B32 host concurrency oracle did not compile'
fi
if ! timeout -k 5 60 "$PRE15_CASE_TMP/B32-cache-oracle" \
>"$artifacts/oracle.stdout" 2>"$artifacts/oracle.stderr"; then
pre15_dut_fail 'B32 host concurrency oracle failed'
fi
sha256sum "$artifacts/B32-source-audit.json" "$artifacts/oracle.stdout" \
>"$artifacts/SHA256SUMS"
fi
pre15_target_reached
if test "${PRE15_MODE:-host}" != qemu; then
printf '%s\n' \
'TC168-cache-inflight host PASS' \
'Canonical key, one owner, waiter bytes/positive errno, retry, eviction, fallback, and shutdown PASS' \
'TC126/TC127/TC129 host oracle NOT_RUN (not necessary)' \
'QEMU and full feature suite NOT_RUN in host mode'
exit 0
fi
for tool in clang cmp diff file mkfs.erofs nm scp tar timeout; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B32 QEMU tool: $tool"
done
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
first=$PRE15_CASE_TMP/fixtures-first
second=$PRE15_CASE_TMP/fixtures-second
if ! (umask 022 && timeout -k 5 180 python3 -B "$generator" --spec "$spec" \
--output "$first") >"$artifacts/generate-first.json" \
2>"$artifacts/generate-first.stderr" || \
! (umask 022 && timeout -k 5 180 python3 -B "$generator" --spec "$spec" \
--output "$second") >"$artifacts/generate-second.json" \
2>"$artifacts/generate-second.stderr"; then
pre15_runner_fail 'B32 fixture generation failed'
fi
if ! diff -qr "$first" "$second" >"$artifacts/fixture-repeat.diff"; then
pre15_runner_fail 'B32 fixtures are not byte reproducible'
fi
cp "$first/fixture-index.json" "$artifacts/B32-fixture-index.json"
module=$PRE15_CASE_TMP/B32-erofs-zstdio0.ko
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
"$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" 0 \
>"$artifacts/kld-build.stdout" 2>"$artifacts/kld-build.stderr"; then
pre15_dut_fail 'B32 cross-target zstdio0 KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/kld-file.txt"
sha256sum "$module" >"$artifacts/kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/kld-nm-u.txt"
fixture_archive=$PRE15_CASE_TMP/B32-fixtures.tar.gz
tar -C "$first" -czf "$fixture_archive" .
pre15_scp()
{
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
}
pre15_guest_ssh_bounded()
{
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" ssh -n -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-p "$PRE15_QEMU_SSH_PORT" \
"$PRE15_QEMU_SSH_USER@127.0.0.1" "$@"
}
if ! pre15_scp "$fixture_archive" /root/B32-fixtures.tar.gz || \
! pre15_scp "$probe" /root/B32-cache-probe.c || \
! pre15_scp "$module" /root/B32-erofs-zstdio0.ko; then
pre15_infra_blocked 'could not transfer B32 module, probe, or fixtures'
fi
if ! pre15_guest_ssh_bounded \
'rm -rf /root/B32-fixtures && mkdir /root/B32-fixtures && tar -xzf /root/B32-fixtures.tar.gz -C /root/B32-fixtures && cc -O2 -Wall -Wextra -Werror -pthread -o /root/B32-cache-probe /root/B32-cache-probe.c'; then
pre15_infra_blocked 'could not prepare B32 guest fixtures/probe'
fi
if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-before-load.txt"
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-before-load.txt"
cache_tunable=vfs.erofs.decoded_cache.minimum_decode_work
cache_tunable_before=$(pre15_guest_ssh_bounded kenv -q "$cache_tunable" || true)
printf 'name=%s before=%s test-value=0\n' "$cache_tunable" \
"${cache_tunable_before:-unset}" >"$artifacts/cache-tunable.txt"
if ! pre15_guest_ssh_bounded kenv "$cache_tunable=0"; then
pre15_infra_blocked 'could not force B32 decoded-cache admission'
fi
if ! pre15_guest_ssh_bounded kldload /root/B32-erofs-zstdio0.ko \
>"$artifacts/kldload.stdout" 2>"$artifacts/kldload.stderr"; then
if test -n "$cache_tunable_before"; then
pre15_guest_ssh_bounded kenv "$cache_tunable=$cache_tunable_before" || true
else
pre15_guest_ssh_bounded kenv -u "$cache_tunable" || true
fi
if grep -q 'module already loaded or in kernel' \
"$artifacts/kldload.stderr"; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
pre15_dut_fail 'B32 exact-source zstdio0 KLD failed to load'
fi
if test -n "$cache_tunable_before"; then
pre15_guest_ssh_bounded kenv "$cache_tunable=$cache_tunable_before" || \
pre15_infra_blocked 'could not restore B32 cache tunable'
else
pre15_guest_ssh_bounded kenv -u "$cache_tunable" || \
pre15_infra_blocked 'could not remove B32 cache tunable override'
fi
cache_tunable_after=$(pre15_guest_ssh_bounded kenv -q "$cache_tunable" || true)
printf 'after=%s\n' "${cache_tunable_after:-unset}" \
>>"$artifacts/cache-tunable.txt"
if test "$cache_tunable_after" != "$cache_tunable_before"; then
pre15_runner_fail 'B32 cache tunable was not restored exactly'
fi
if ! pre15_guest_ssh_bounded kldstat -q -m erofs; then
pre15_dut_fail 'B32 kldload returned success without erofs.1 ownership'
fi
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-after-load.txt"
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-before.txt"
diff -u "$artifacts/guest-dmesg-before-load.txt" \
"$artifacts/guest-dmesg-before.txt" >"$artifacts/guest-dmesg-load.diff" || true
sed -n '/^+++ /d; /^+/s/^+//p' "$artifacts/guest-dmesg-load.diff" \
>"$artifacts/guest-dmesg-load-added.txt"
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free|fatal trap|pager fault|linker.*(error|undefined)|undefined symbol' \
"$artifacts/guest-dmesg-load-added.txt"; then
pre15_dut_fail 'B32 exact-source KLD load produced kernel or linker errors'
fi
pre15_own_guest_kld B32-erofs-zstdio0.ko 'B32 exact-source KLD'
b32_unown()
{
kind=$1
value=$2
tmp=$PRE15_CASE_TMP/ownership.$$
awk -F ' ' -v kind="$kind" -v value="$value" \
'!($1 == kind && $2 == value)' "$PRE15_OWNERSHIP_FILE" >"$tmp"
mv "$tmp" "$PRE15_OWNERSHIP_FILE"
}
b32_attach()
{
image=$1
label=$2
B32_MD=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
-f "/root/B32-fixtures/images/$image") || \
pre15_dut_fail "$label md attach failed"
case "$B32_MD" in
md[0-9]*) ;;
*) pre15_runner_fail "unexpected B32 md unit: $B32_MD" ;;
esac
pre15_own_guest_md "$B32_MD" "$label md"
B32_MOUNT=/mnt/pre15-b32-$label
pre15_guest_ssh_bounded mkdir -p "$B32_MOUNT"
pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$B32_MD" "$B32_MOUNT" || \
pre15_dut_fail "$label mount failed"
pre15_own_guest_mount "$B32_MOUNT" "$label mount"
}
b32_detach()
{
if pre15_guest_ssh_bounded mount | grep -F " on $B32_MOUNT " >/dev/null; then
pre15_guest_ssh_bounded umount "$B32_MOUNT" || \
pre15_dut_fail "$B32_MOUNT unmount failed"
fi
b32_unown guest-mount "$B32_MOUNT"
pre15_guest_ssh_bounded mdconfig -d -u "$B32_MD" || \
pre15_dut_fail "$B32_MD detach failed"
b32_unown guest-md "$B32_MD"
}
b32_start_lifecycle()
{
label=$1
B32_CONTROL=/tmp/B32-$label-control
B32_LIFECYCLE_PID=$(pre15_guest_ssh_bounded \
"rm -rf '$B32_CONTROL'; mkdir '$B32_CONTROL'; nohup /root/B32-cache-probe lifecycle '$B32_MOUNT/payload.bin' '$B32_CONTROL' >/tmp/B32-$label.stdout 2>/tmp/B32-$label.stderr </dev/null & echo \$!") || \
pre15_infra_blocked "$label lifecycle process did not start"
case "$B32_LIFECYCLE_PID" in
''|*[!0-9]*) pre15_runner_fail "invalid B32 lifecycle PID: $B32_LIFECYCLE_PID" ;;
esac
pre15_guest_ssh_bounded \
"i=0; while test ! -f '$B32_CONTROL/ready'; do i=\$((i + 1)); test \$i -lt 600 || exit 1; sleep 0.1; done" || \
pre15_infra_blocked "$label lifecycle ready marker timed out"
}
b32_stop_lifecycle()
{
pre15_guest_ssh_bounded touch "$B32_CONTROL/exit"
pre15_guest_ssh_bounded \
"i=0; while kill -0 '$B32_LIFECYCLE_PID' 2>/dev/null; do i=\$((i + 1)); test \$i -lt 600 || exit 1; sleep 0.1; done" || \
pre15_infra_blocked 'B32 lifecycle process did not exit'
}
reference=/root/B32-fixtures/source/payload.bin
b32_attach lzma-valid.erofs valid
if ! pre15_guest_ssh_bounded /root/B32-cache-probe concurrent \
"$B32_MOUNT/payload.bin" "$reference" 0 64 8 0 65536 \
>"$artifacts/valid-concurrent.stdout" \
2>"$artifacts/valid-concurrent.stderr"; then
pre15_dut_fail 'TC168 valid same-key concurrent reads diverged'
fi
if ! grep -qx 'concurrent PASS workers=64 loops=8 assertions=512 errno=0 offset=0 length=65536' \
"$artifacts/valid-concurrent.stdout"; then
pre15_runner_fail 'TC168 valid assertion count was not exactly 512'
fi
if test "$scenario" = TC184-vnode-races; then
if ! pre15_guest_ssh_bounded /root/B32-cache-probe vnode-race \
"$B32_MOUNT/payload.bin" "$reference" 64 50 \
>"$artifacts/vnode-race.stdout" \
2>"$artifacts/vnode-race.stderr"; then
pre15_dut_fail 'TC184 lookup/open/read/reclaim loop diverged'
fi
if ! grep -qx 'vnode-race PASS workers=64 loops=50 assertions=3200' \
"$artifacts/vnode-race.stdout"; then
pre15_runner_fail 'TC184 vnode assertion count was not exactly 3200'
fi
fi
b32_detach
if test "$scenario" = TC168-cache-inflight; then
b32_attach lzma-truncated.erofs truncated
if ! pre15_guest_ssh_bounded /root/B32-cache-probe concurrent \
"$B32_MOUNT/payload.bin" "$reference" 97 1 1 0 65536 \
>"$artifacts/failure-single.stdout" \
2>"$artifacts/failure-single.stderr"; then
pre15_dut_fail 'TC168 single corrupt read did not return EINTEGRITY'
fi
if ! grep -qx 'concurrent PASS workers=1 loops=1 assertions=1 errno=97 offset=0 length=65536' \
"$artifacts/failure-single.stdout"; then
pre15_runner_fail 'TC168 single corrupt assertion count was not 1'
fi
if ! pre15_guest_ssh_bounded /root/B32-cache-probe concurrent \
"$B32_MOUNT/payload.bin" "$reference" 97 64 2 0 65536 \
>"$artifacts/failure-concurrent.stdout" \
2>"$artifacts/failure-concurrent.stderr"; then
pre15_dut_fail 'TC168 failure waiters did not receive EINTEGRITY'
fi
if ! grep -qx 'concurrent PASS workers=64 loops=2 assertions=128 errno=97 offset=0 length=65536' \
"$artifacts/failure-concurrent.stdout"; then
pre15_runner_fail 'TC168 failure assertion count was not exactly 128'
fi
b32_detach
else
b32_attach lzma-valid.erofs normal-busy
b32_start_lifecycle normal-busy
if pre15_guest_ssh_bounded umount "$B32_MOUNT" \
>"$artifacts/normal-unmount.stdout" 2>"$artifacts/normal-unmount.stderr"; then
pre15_dut_fail 'TC184 normal unmount with held vnode unexpectedly succeeded'
fi
if ! grep -qi 'busy' "$artifacts/normal-unmount.stderr"; then
pre15_dut_fail 'TC184 normal unmount did not report EBUSY'
fi
b32_stop_lifecycle
b32_detach
b32_attach lzma-valid.erofs forced-held
b32_start_lifecycle forced-held
pre15_guest_ssh_bounded umount -f "$B32_MOUNT" || \
pre15_dut_fail 'TC184 held-vnode forced unmount command failed'
b32_unown guest-mount "$B32_MOUNT"
pre15_guest_ssh_bounded touch "$B32_CONTROL/read"
pre15_guest_ssh_bounded \
"i=0; while test ! -f '$B32_CONTROL/result'; do i=\$((i + 1)); test \$i -lt 600 || exit 1; sleep 0.1; done" || \
pre15_infra_blocked 'TC184 held-vnode result timed out'
pre15_guest_ssh_bounded cat "$B32_CONTROL/result" \
>"$artifacts/forced-held-result.txt"
if ! grep -qx -- '-1 6' "$artifacts/forced-held-result.txt"; then
pre15_dut_fail 'TC184 revoked held vnode did not return exact ENXIO'
fi
b32_detach
b32_attach lzma-valid.erofs forced-closed
b32_start_lifecycle forced-closed
pre15_guest_ssh_bounded touch "$B32_CONTROL/close"
pre15_guest_ssh_bounded \
"i=0; while test ! -f '$B32_CONTROL/closed'; do i=\$((i + 1)); test \$i -lt 600 || exit 1; sleep 0.1; done" || \
pre15_infra_blocked 'TC184 closed-descriptor marker timed out'
pre15_guest_ssh_bounded umount -f "$B32_MOUNT" || \
pre15_dut_fail 'TC184 closed-descriptor forced unmount command failed'
b32_unown guest-mount "$B32_MOUNT"
pre15_guest_ssh_bounded touch "$B32_CONTROL/read"
pre15_guest_ssh_bounded \
"i=0; while test ! -f '$B32_CONTROL/result'; do i=\$((i + 1)); test \$i -lt 600 || exit 1; sleep 0.1; done" || \
pre15_infra_blocked 'TC184 closed-descriptor result timed out'
pre15_guest_ssh_bounded cat "$B32_CONTROL/result" \
>"$artifacts/forced-closed-result.txt"
if ! grep -qx -- '-1 9' "$artifacts/forced-closed-result.txt"; then
pre15_dut_fail 'TC184 closed descriptor did not return exact EBADF'
fi
b32_detach
fi
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-after.txt"
diff -u "$artifacts/guest-dmesg-before.txt" \
"$artifacts/guest-dmesg-after.txt" >"$artifacts/guest-dmesg.diff" || true
sed -n '/^+++ /d; /^+/s/^+//p' "$artifacts/guest-dmesg.diff" \
>"$artifacts/guest-dmesg-added.txt"
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free|fatal trap|pager fault|linker.*(error|undefined)|undefined symbol' \
"$artifacts/guest-dmesg-added.txt"; then
pre15_dut_fail "$scenario produced kernel, linker, WITNESS, or UAF evidence"
fi
printf '%s PASS\n' "$scenario"
printf '%s\n' \
'QEMU used an owned overlay and dynamic forwarded port' \
'KLD erofs.1 ownership and cache tunable restoration PASS' \
'TC130 and full feature suite NOT_RUN'
+641
View File
@@ -0,0 +1,641 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=93d0c64e321f50cf68026c3f1c0c95293bf17302
case_script=$PRE15_DUT/tests/pre15/cases/B33-cache-policy.sh
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
gate_dir=$PRE15_DUT/tests/pre15/gates
oracle=$fixture_dir/B33-cache-oracle.c
qemu_spec=$fixture_dir/B33-cache-state.json
qemu_generator=$fixture_dir/B33-cache-generate.py
qemu_runner=$fixture_dir/B33-qemu-run.sh
b32_probe=$fixture_dir/B32-cache-probe.c
kld_builder=$fixture_dir/B28-build-kld.sh
gate_input=$gate_dir/P15-038-input.json
gate_result=$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038/result.json
gate_benefit=$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038/benefit.json
gate_state=$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038/state-model.json
gate_hashes=$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038/SHA256SUMS
artifacts=$PRE15_RUN_DIR/artifacts
for tool in cc git python3 sha256sum timeout; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B33 host tool: $tool"
done
for fixture in "$case_script" "$oracle" "$qemu_spec" "$qemu_generator" "$qemu_runner" \
"$b32_probe" "$kld_builder" "$gate_input" "$gate_result" \
"$gate_benefit" "$gate_state" "$gate_hashes"; do
pre15_record_fixture "b33-$(basename "$fixture")" "$fixture"
done
for source in internal.h zdata.c; do
pre15_record_fixture "b33-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"
if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
pre15_target_reached
else
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$gate_input" \
"$gate_result" "$gate_benefit" "$gate_state" \
"$artifacts/B33-source-audit.json" <<'PY'
from __future__ import annotations
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
baseline = sys.argv[3]
implementation = "bc56f830918b76029871b60cca2e53992de70a2e"
current_owners = {
"internal.h": "aa20623132b6821c6ad4651b263fc0ac0f7a14c8",
"zdata.c": implementation,
}
gate_input = json.loads(Path(sys.argv[4]).read_text(encoding="ascii"))
gate_result = json.loads(Path(sys.argv[5]).read_text(encoding="ascii"))
gate_benefit = json.loads(Path(sys.argv[6]).read_text(encoding="ascii"))
gate_state = json.loads(Path(sys.argv[7]).read_text(encoding="ascii"))
report_path = Path(sys.argv[8])
def fail(message: str) -> None:
raise SystemExit(message)
def committed(commit: str, relative: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{commit}:{relative}"],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=30,
)
if completed.returncode != 0:
fail(f"cannot read B33 source {commit}:{relative}: {completed.stderr.strip()}")
return completed.stdout
def function(source: str, name: str) -> str:
match = re.search(rf"\n(?:static\s+)?[^\n]+\n{name}\([^{{]+\n\{{", source)
if match is None:
fail(f"cannot locate function {name}")
start = match.start() + 1
brace = source.find("{", match.start())
depth = 0
for index in range(brace, len(source)):
if source[index] == "{":
depth += 1
elif source[index] == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
fail(f"unterminated function {name}")
expected_changed = {
"repo-pre-15/src/internal.h",
"repo-pre-15/src/zdata.c",
"repo-pre-15/tests/pre15/cases/B33-cache-policy.sh",
"repo-pre-15/tests/pre15/fixtures/B33-cache-generate.py",
"repo-pre-15/tests/pre15/fixtures/B33-cache-oracle.c",
"repo-pre-15/tests/pre15/fixtures/B33-cache-state.json",
"repo-pre-15/tests/pre15/fixtures/B33-qemu-run.sh",
}
changed = subprocess.run(
[
"git",
"-C",
str(root),
"diff-tree",
"--no-commit-id",
"--name-only",
"-r",
implementation,
],
check=True,
stdout=subprocess.PIPE,
text=True,
timeout=30,
).stdout.splitlines()
if set(changed) != expected_changed or len(changed) != len(expected_changed):
fail(f"B33 implementation write set mismatch: {changed!r}")
for name, commit in (("baseline", baseline), ("implementation", implementation)):
if subprocess.check_output(
["git", "-C", str(root), "rev-parse", commit], text=True
).strip() != commit:
fail(f"B33 {name} identity changed")
internal = (dut / "src/internal.h").read_text(encoding="utf-8")
zdata = (dut / "src/zdata.c").read_text(encoding="utf-8")
implemented_internal = committed(implementation, "repo-pre-15/src/internal.h")
implemented_zdata = committed(implementation, "repo-pre-15/src/zdata.c")
for name, source in (("internal.h", internal), ("zdata.c", zdata)):
owner = current_owners[name]
if source != committed(owner, f"repo-pre-15/src/{name}"):
fail(f"current B33 source differs from accepted owner {owner}: {name}")
baseline_zdata = committed(baseline, "repo-pre-15/src/zdata.c")
for preserved in ("z_erofs_decode_length", "z_erofs_decode_extent"):
if function(implemented_zdata, preserved) != function(baseline_zdata, preserved):
fail(f"B25/B27/B28 read/decode path changed in B33: {preserved}")
do_read = function(implemented_zdata, "z_erofs_do_read")
baseline_do_read = function(baseline_zdata, "z_erofs_do_read")
bypass_delta = (
"\t\t\t} else\n"
"\t\t\t\tz_erofs_extent_cache_record_bypass(sbi, &map);"
)
if do_read.count(bypass_delta) != 1 or do_read.replace(bypass_delta, "\t\t\t}") != baseline_do_read:
fail("B33 z_erofs_do_read differs beyond policy-rejection accounting")
required_b32 = (
"cache->nid == vi->nid",
"cache->decoded_size == decoded_size",
"cache->map.m_pa == map->m_pa",
"cache->map.m_la == map->m_la",
"cache->map.m_plen == map->m_plen",
"cache->map.m_llen == map->m_llen",
"cache->map.m_deviceid == map->m_deviceid",
"cache->map.m_algorithmformat == map->m_algorithmformat",
"cache->map.m_flags == map->m_flags",
"cache->state == EROFS_ZCACHE_INFLIGHT || cache->waiters != 0",
"cv_wait(&cache->cv, &sbi->z_extent_cache_lock)",
"cache->error = error",
"cache->closing = true",
"Z_EROFS_CACHE_BYPASS",
"free(decoded, M_EROFS);",
)
missing = [token for token in required_b32 if token not in zdata]
if missing:
fail(f"B32 key/inflight/failure/fallback token missing: {missing!r}")
policy = function(zdata, "z_erofs_extent_cache_eligible")
for named_codec in (
"Z_EROFS_COMPRESSION_LZ4",
"Z_EROFS_COMPRESSION_LZMA",
"Z_EROFS_COMPRESSION_DEFLATE",
"Z_EROFS_COMPRESSION_ZSTD",
):
if named_codec in policy:
fail(f"B33 admission still names a codec: {named_codec}")
for token in (
"map->m_algorithmformat < Z_EROFS_COMPRESSION_MAX",
"len <= cache->budget_bytes",
"decode_work >= cache->minimum_decode_work",
"z_erofs_cache_enabled != 0",
):
if token not in policy:
fail(f"codec-neutral work/size policy token is absent: {token}")
if "map->m_algorithmformat == Z_EROFS_COMPRESSION_LZMA" in zdata:
fail("LZMA-only admission branch remains")
record_bypass = function(zdata, "z_erofs_extent_cache_record_bypass")
if "++cache->metrics[map->m_algorithmformat].bypasses" not in record_bypass or (
"z_erofs_extent_cache_record_bypass(sbi, &map)" not in do_read
):
fail("B33 policy rejection is absent from four-codec accounting")
budget = gate_input["budget"]
expected_macros = {
"EROFS_ZCACHE_MOUNT_HARD_BUDGET": budget["per_mount_bytes"] // 1024,
"EROFS_ZCACHE_GLOBAL_HARD_BUDGET": budget["global_bytes"] // 1024,
"EROFS_ZCACHE_MIN_DECODE_WORK": budget["minimum_decode_work_bytes"] // 1024,
}
for name, kib in expected_macros.items():
if f"#define {name} ({kib}UL * 1024)" not in zdata:
fail(f"B33 hard policy differs from G05: {name}")
for token in (
'TUNABLE_INT("vfs.erofs.decoded_cache.enabled"',
'TUNABLE_ULONG("vfs.erofs.decoded_cache.mount_budget"',
'TUNABLE_ULONG("vfs.erofs.decoded_cache.global_budget"',
'TUNABLE_ULONG("vfs.erofs.decoded_cache.minimum_decode_work"',
"MIN(z_erofs_cache_mount_budget,",
"MIN(z_erofs_cache_global_budget,",
"EVENTHANDLER_REGISTER(vm_lowmem",
"EVENTHANDLER_DEREGISTER(vm_lowmem",
"LIST_FOREACH(sbi, &z_erofs_cache_mounts, z_extent_cache_link)",
"mtx_trylock(&sbi->z_extent_cache_lock)",
):
if token not in zdata:
fail(f"B33 budget/config/reclaim token is absent: {token}")
claim = function(zdata, "z_erofs_extent_cache_claim")
if claim.find("z_erofs_extent_cache_reserve(decoded_size)") > claim.find(
"cache->state = EROFS_ZCACHE_INFLIGHT"
):
fail("B33 does not reserve before inflight decode")
drop_position = claim.find("z_erofs_extent_cache_drop_locked(cache, true, false)")
empty_position = claim.find("cache->state = EROFS_ZCACHE_EMPTY", drop_position)
reserve_position = claim.find("z_erofs_extent_cache_reserve(decoded_size)")
if drop_position < 0 or not (drop_position < empty_position < reserve_position):
fail("B33 replacement reservation failure can leave READY without data")
complete = function(zdata, "z_erofs_extent_cache_complete")
if complete.find("z_erofs_extent_cache_release(cache->charged_bytes)") > complete.find(
"cache->state = EROFS_ZCACHE_FAILED"
):
fail("B33 failed decode publishes before releasing reservation")
for field in (
"hits",
"misses",
"bypasses",
"evictions",
"reclaims",
"resident_bytes",
"charged_bytes",
"budget_bytes",
"minimum_decode_work",
"metrics[Z_EROFS_COMPRESSION_MAX]",
):
if field not in internal:
fail(f"B33 four-codec accounting field is absent: {field}")
for forbidden in ("shrinker", "workqueue", "work_struct", "wait_on_bit", "folio"):
if forbidden in internal + zdata:
fail(f"Linux-only cache primitive introduced: {forbidden}")
if gate_result.get("status") != "GO" or gate_result.get("b33") != "AUTHORIZED":
fail("P15-038 G05 result is not GO")
if gate_state.get("status") != "PASS" or gate_state["budget"]["global_remaining"] != 0:
fail("P15-038 state model is not closed")
if gate_benefit.get("codecs_meeting_threshold", 0) < 2 or any(
len(summary["current_ns"]) != 5 or len(summary["candidate_ns"]) != 5
for summary in gate_benefit.get("summaries", [])
):
fail("P15-038 fixed five-sample benefit is incomplete")
report = {
"baseline": baseline,
"batch": "B33",
"b32_contract_preserved": True,
"codec_neutral": True,
"current_owners": current_owners,
"gate_codecs_meeting_threshold": gate_benefit["codecs_meeting_threshold"],
"global_hard_budget": budget["global_bytes"],
"implementation": implementation,
"lock_order": ["global-list", "mount-cache", "global-budget"],
"mount_hard_budget": budget["per_mount_bytes"],
"no_cache_fallback": True,
"reclaim": "FreeBSD-vm_lowmem",
"write_set": sorted(changed),
}
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
print(json.dumps(report, indent=2, sort_keys=True))
PY
then
pre15_runner_fail 'B33 source, gate, write-set, or preservation audit failed'
fi
if ! (cd "$PRE15_ROOT/planning/pre15/evidence/20260816T021736Z-G05-P15-038" && \
timeout -k 5 30 sha256sum -c SHA256SUMS) \
>"$artifacts/gate-manifest.stdout" 2>"$artifacts/gate-manifest.stderr"; then
pre15_runner_fail 'P15-038 gate evidence manifest failed'
fi
if ! cc -std=c11 -O2 -Wall -Wextra -Werror "$oracle" \
-o "$PRE15_CASE_TMP/B33-cache-oracle" \
>"$artifacts/oracle-build.stdout" 2>"$artifacts/oracle-build.stderr"; then
pre15_runner_fail 'B33 cache budget oracle did not compile'
fi
if ! timeout -k 5 60 "$PRE15_CASE_TMP/B33-cache-oracle" \
>"$artifacts/oracle.json" 2>"$artifacts/oracle.stderr"; then
pre15_dut_fail 'B33 cache budget/accounting oracle failed'
fi
if ! python3 -B - "$artifacts/oracle.json" <<'PY'
import json
from pathlib import Path
import sys
result = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
if result.get("status") != "PASS":
raise SystemExit("B33 oracle status is not PASS")
if result.get("global_peak") != result.get("global_limit") or result.get("global_remaining") != 0:
raise SystemExit("B33 oracle global budget did not close")
metrics = result.get("metrics", [])
if len(metrics) != 4 or [item.get("codec") for item in metrics] != list(range(4)):
raise SystemExit("B33 four-codec metric set is incomplete")
for item in metrics:
if item.get("hits", 0) < 1 or item.get("misses", 0) < 1 or item.get("evictions", 0) < 1:
raise SystemExit(f"B33 codec metric is incomplete: {item!r}")
if item.get("resident_bytes") != 0:
raise SystemExit(f"B33 codec resident bytes leaked: {item!r}")
PY
then
pre15_dut_fail 'B33 four-codec accounting result is incomplete'
fi
sha256sum "$artifacts/B33-source-audit.json" "$artifacts/oracle.json" \
"$artifacts/gate-manifest.stdout" >"$artifacts/SHA256SUMS"
fi
pre15_target_reached
if test "${PRE15_MODE:-host}" != qemu; then
printf '%s\n' \
'B33-cache-budget host PASS' \
'Four-codec admission/accounting, mount/global hard budgets, exhaustion fallback, reclaim, failure retry, disable, and eviction hash PASS' \
'P15-038 host codec-cost gate replay manifest PASS; no guest vnode performance claimed' \
'TC084/TC105/TC129 NOT_RUN (not necessary)' \
'QEMU TC168, TC130, and full feature suite NOT_RUN in host mode'
exit 0
fi
test "${PRE15_B32_SCENARIO:-}" = TC168-cache-inflight || \
pre15_runner_fail 'B33 QEMU mode requires TC168-cache-inflight'
for tool in clang cmp diff file mkfs.erofs nm scp tar timeout; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B33 QEMU tool: $tool"
done
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
first=$PRE15_CASE_TMP/fixtures-first
second=$PRE15_CASE_TMP/fixtures-second
if ! (umask 022 && timeout -k 5 180 python3 -B "$qemu_generator" \
--spec "$qemu_spec" --output "$first") \
>"$artifacts/generate-first.json" 2>"$artifacts/generate-first.stderr" || \
! (umask 022 && timeout -k 5 180 python3 -B "$qemu_generator" \
--spec "$qemu_spec" --output "$second") \
>"$artifacts/generate-second.json" 2>"$artifacts/generate-second.stderr"; then
pre15_runner_fail 'B33 fixture generation failed'
fi
if ! diff -qr "$first" "$second" >"$artifacts/fixture-repeat.diff"; then
pre15_runner_fail 'B33 fixtures are not byte reproducible'
fi
cp "$first/fixture-index.json" "$artifacts/B33-fixture-index.json"
module=$PRE15_CASE_TMP/B33-erofs-zstdio0.ko
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
"$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" 0 \
>"$artifacts/kld-build.stdout" 2>"$artifacts/kld-build.stderr"; then
pre15_dut_fail 'B33 cross-target zstdio0 KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/kld-file.txt"
sha256sum "$module" >"$artifacts/kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/kld-nm-u.txt"
fixture_archive=$PRE15_CASE_TMP/B33-fixtures.tar.gz
tar -C "$first" -czf "$fixture_archive" .
pre15_scp()
{
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
}
pre15_guest_ssh_bounded()
{
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" ssh -n -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-p "$PRE15_QEMU_SSH_PORT" \
"$PRE15_QEMU_SSH_USER@127.0.0.1" "$@"
}
if ! pre15_scp "$fixture_archive" /root/B33-fixtures.tar.gz || \
! pre15_scp "$b32_probe" /root/B33-cache-probe.c || \
! pre15_scp "$module" /root/B33-erofs-zstdio0.ko; then
pre15_infra_blocked 'could not transfer B33 module, probe, or fixtures'
fi
if ! pre15_guest_ssh_bounded \
'rm -rf /root/B33-fixtures && mkdir /root/B33-fixtures && tar -xzf /root/B33-fixtures.tar.gz -C /root/B33-fixtures && cc -O2 -Wall -Wextra -Werror -pthread -o /root/B33-cache-probe /root/B33-cache-probe.c'; then
pre15_infra_blocked 'could not prepare B33 guest fixtures/probe'
fi
if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
module_sha256=$(sha256sum "$module" | awk '{print $1}')
guest_module_sha256=$(pre15_guest_ssh_bounded sha256 -q /root/B33-erofs-zstdio0.ko) || \
pre15_infra_blocked 'could not hash transferred B33 KLD'
printf 'host=%s guest=%s\n' "$module_sha256" "$guest_module_sha256" \
>"$artifacts/guest-module-sha256.txt"
test "$guest_module_sha256" = "$module_sha256" || \
pre15_runner_fail 'transferred B33 KLD hash differs from host module'
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-before-load.txt"
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-before-load.txt"
cache_tunable=vfs.erofs.decoded_cache.minimum_decode_work
cache_tunable_before=$(pre15_guest_ssh_bounded kenv -q "$cache_tunable" || true)
printf 'name=%s before=%s test-value=0\n' "$cache_tunable" \
"${cache_tunable_before:-unset}" >"$artifacts/cache-tunable.txt"
if ! pre15_guest_ssh_bounded kenv "$cache_tunable=0" \
>"$artifacts/cache-tunable-set.stdout" \
2>"$artifacts/cache-tunable-set.stderr"; then
pre15_infra_blocked 'could not force B33 decoded-cache admission'
fi
if ! pre15_guest_ssh_bounded kldload /root/B33-erofs-zstdio0.ko \
>"$artifacts/kldload.stdout" 2>"$artifacts/kldload.stderr"; then
if test -n "$cache_tunable_before"; then
pre15_guest_ssh_bounded kenv "$cache_tunable=$cache_tunable_before" || true
else
pre15_guest_ssh_bounded kenv -u "$cache_tunable" || true
fi
if grep -q 'module already loaded or in kernel' \
"$artifacts/kldload.stderr"; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
pre15_dut_fail 'B33 exact-source zstdio0 KLD failed to load'
fi
if test -n "$cache_tunable_before"; then
pre15_guest_ssh_bounded kenv "$cache_tunable=$cache_tunable_before" \
>"$artifacts/cache-tunable-restore.stdout" \
2>"$artifacts/cache-tunable-restore.stderr" || \
pre15_infra_blocked 'could not restore B33 cache tunable'
else
pre15_guest_ssh_bounded kenv -u "$cache_tunable" \
>"$artifacts/cache-tunable-restore.stdout" \
2>"$artifacts/cache-tunable-restore.stderr" || \
pre15_infra_blocked 'could not remove B33 cache tunable override'
fi
cache_tunable_after=$(pre15_guest_ssh_bounded kenv -q "$cache_tunable" || true)
printf 'after=%s\n' "${cache_tunable_after:-unset}" \
>>"$artifacts/cache-tunable.txt"
if test "$cache_tunable_after" != "$cache_tunable_before"; then
pre15_runner_fail 'B33 cache tunable was not restored exactly'
fi
if ! pre15_guest_ssh_bounded kldstat -q -m erofs; then
pre15_dut_fail 'B33 kldload returned success without erofs.1 ownership'
fi
if ! pre15_guest_ssh_bounded kldstat -q -n B33-erofs-zstdio0.ko; then
pre15_dut_fail 'B33 kldload returned success without exact KLD file ownership'
fi
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-after-load.txt"
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-before.txt"
diff -u "$artifacts/guest-dmesg-before-load.txt" \
"$artifacts/guest-dmesg-before.txt" >"$artifacts/guest-dmesg-load.diff" || true
sed -n '/^+++ /d; /^+/s/^+//p' "$artifacts/guest-dmesg-load.diff" \
>"$artifacts/guest-dmesg-load-added.txt"
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free|fatal trap|pager fault|linker.*(error|undefined)|undefined symbol' \
"$artifacts/guest-dmesg-load-added.txt"; then
pre15_dut_fail 'B33 exact-source KLD load produced kernel or linker errors'
fi
pre15_own_guest_kld B33-erofs-zstdio0.ko 'B33 exact-source KLD'
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-before-trace.txt"
if ! pre15_guest_ssh_bounded command -v dtrace \
>"$artifacts/guest-dtrace-command.txt" 2>"$artifacts/guest-dtrace-command.stderr"; then
pre15_infra_blocked 'guest dtrace tool is unavailable for B33 call proof'
fi
set +e
pre15_guest_ssh_bounded kldload dtraceall \
>"$artifacts/guest-dtrace-kldload.stdout" \
2>"$artifacts/guest-dtrace-kldload.stderr"
dtrace_load_rc=$?
set -e
pre15_guest_ssh_bounded kldstat >"$artifacts/guest-kldstat-after-trace.txt"
awk 'NR == FNR { if (FNR > 1) seen[$5] = 1; next }
FNR > 1 && !seen[$5] { print $1 "\t" $5 }' \
"$artifacts/guest-kldstat-before-trace.txt" \
"$artifacts/guest-kldstat-after-trace.txt" \
>"$artifacts/guest-trace-klds-all.tsv"
awk '$2 == "dtraceall.ko"' "$artifacts/guest-trace-klds-all.tsv" \
>"$artifacts/guest-trace-klds.tsv"
while IFS="$(printf '\t')" read -r trace_kld_id trace_kld_name; do
test -n "$trace_kld_id" || continue
pre15_own_guest_kld "$trace_kld_name" 'B33 FBT call-proof dependency'
done <"$artifacts/guest-trace-klds.tsv"
test "$dtrace_load_rc" -eq 0 || \
pre15_infra_blocked 'guest dtraceall KLD failed to load for B33 call proof'
if ! pre15_guest_ssh_bounded \
"dtrace -l -n 'fbt::z_erofs_extent_cache_init:entry' -n 'fbt::z_erofs_do_read:entry'" \
>"$artifacts/guest-dtrace-probe-list.txt" \
2>"$artifacts/guest-dtrace-probe-list.stderr"; then
pre15_infra_blocked 'B33 cache init/read FBT probes are unavailable'
fi
grep -q 'z_erofs_extent_cache_init.*entry' \
"$artifacts/guest-dtrace-probe-list.txt" || \
pre15_runner_fail 'B33 cache init FBT probe was not listed exactly'
grep -q 'z_erofs_do_read.*entry' \
"$artifacts/guest-dtrace-probe-list.txt" || \
pre15_runner_fail 'B33 compressed-read FBT probe was not listed exactly'
b33_unown()
{
kind=$1
value=$2
tmp=$PRE15_CASE_TMP/ownership.$$
awk -F ' ' -v kind="$kind" -v value="$value" \
'!($1 == kind && $2 == value)' "$PRE15_OWNERSHIP_FILE" >"$tmp"
mv "$tmp" "$PRE15_OWNERSHIP_FILE"
}
b33_attach()
{
image=$1
label=$2
B33_MD=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
-f "/root/B33-fixtures/images/$image") || \
pre15_dut_fail "$label md attach failed"
case "$B33_MD" in
md[0-9]*) ;;
*) pre15_runner_fail "unexpected B33 md unit: $B33_MD" ;;
esac
pre15_own_guest_md "$B33_MD" "$label md"
B33_MOUNT=/mnt/pre15-b33-$label
pre15_guest_ssh_bounded mkdir -p "$B33_MOUNT"
if test "${B33_TRACE_MOUNT:-0}" = 1; then
mount_trace=/tmp/pre15-b33-cache-init.trace
pre15_guest_ssh_bounded \
"dtrace -q -o '$mount_trace' -n 'fbt::z_erofs_extent_cache_init:entry { @calls = count(); } END { printa(@calls); }' -c 'mount -t erofs -o ro /dev/$B33_MD $B33_MOUNT'" \
>"$artifacts/valid-mount.stdout" \
2>"$artifacts/valid-mount.stderr" || \
pre15_dut_fail "$label traced mount failed"
pre15_guest_ssh_bounded cat "$mount_trace" \
>"$artifacts/guest-cache-init-fbt.txt"
if ! awk '$1 ~ /^[0-9]+$/ && $1 > 0 { found = 1 }
END { exit !found }' "$artifacts/guest-cache-init-fbt.txt"; then
pre15_dut_fail 'B33 guest mount did not call decoded-cache init'
fi
else
pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$B33_MD" "$B33_MOUNT" || \
pre15_dut_fail "$label mount failed"
fi
pre15_own_guest_mount "$B33_MOUNT" "$label mount"
}
b33_detach()
{
if pre15_guest_ssh_bounded mount | grep -F " on $B33_MOUNT " >/dev/null; then
pre15_guest_ssh_bounded umount "$B33_MOUNT" || \
pre15_dut_fail "$B33_MOUNT unmount failed"
fi
b33_unown guest-mount "$B33_MOUNT"
pre15_guest_ssh_bounded mdconfig -d -u "$B33_MD" || \
pre15_dut_fail "$B33_MD detach failed"
b33_unown guest-md "$B33_MD"
}
reference=/root/B33-fixtures/source/payload.bin
B33_TRACE_MOUNT=1
b33_attach lz4-valid.erofs valid
unset B33_TRACE_MOUNT
trace_file=/tmp/pre15-b33-do-read.trace
if ! pre15_guest_ssh_bounded \
"dtrace -q -o '$trace_file' -n 'fbt::z_erofs_do_read:entry { @calls = count(); } END { printa(@calls); }' -c '/root/B33-cache-probe concurrent $B33_MOUNT/payload.bin $reference 0 64 4 0 65536'" \
>"$artifacts/valid-concurrent.stdout" \
2>"$artifacts/valid-concurrent.stderr"; then
pre15_dut_fail 'TC168 valid full-extent concurrent reads diverged'
fi
if ! grep -qx 'concurrent PASS workers=64 loops=4 assertions=256 errno=0 offset=0 length=65536' \
"$artifacts/valid-concurrent.stdout"; then
pre15_runner_fail 'TC168 valid assertion count was not exactly 256'
fi
pre15_guest_ssh_bounded cat "$trace_file" \
>"$artifacts/guest-do-read-fbt.txt"
if ! awk '$1 ~ /^[0-9]+$/ && $1 > 0 { found = 1 }
END { exit !found }' "$artifacts/guest-do-read-fbt.txt"; then
pre15_dut_fail 'B33 guest reads did not call the target read implementation'
fi
b33_detach
b33_attach lz4-truncated.erofs truncated
if ! pre15_guest_ssh_bounded /root/B33-cache-probe concurrent \
"$B33_MOUNT/payload.bin" "$reference" 97 1 1 0 65536 \
>"$artifacts/failure-single.stdout" \
2>"$artifacts/failure-single.stderr"; then
pre15_dut_fail 'TC168 single corrupt read did not return EINTEGRITY'
fi
if ! grep -qx 'concurrent PASS workers=1 loops=1 assertions=1 errno=97 offset=0 length=65536' \
"$artifacts/failure-single.stdout"; then
pre15_runner_fail 'TC168 single corrupt assertion count was not 1'
fi
if ! pre15_guest_ssh_bounded /root/B33-cache-probe concurrent \
"$B33_MOUNT/payload.bin" "$reference" 97 64 2 0 65536 \
>"$artifacts/failure-concurrent.stdout" \
2>"$artifacts/failure-concurrent.stderr"; then
pre15_dut_fail 'TC168 failure waiters did not receive EINTEGRITY'
fi
if ! grep -qx 'concurrent PASS workers=64 loops=2 assertions=128 errno=97 offset=0 length=65536' \
"$artifacts/failure-concurrent.stdout"; then
pre15_runner_fail 'TC168 failure assertion count was not exactly 128'
fi
b33_detach
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg-after.txt"
diff -u "$artifacts/guest-dmesg-before.txt" \
"$artifacts/guest-dmesg-after.txt" >"$artifacts/guest-dmesg.diff" || true
sed -n '/^+++ /d; /^+/s/^+//p' "$artifacts/guest-dmesg.diff" \
>"$artifacts/guest-dmesg-added.txt"
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free|fatal trap|pager fault|linker.*(error|undefined)|undefined symbol' \
"$artifacts/guest-dmesg-added.txt"; then
pre15_dut_fail 'TC168 produced panic, WITNESS, or UAF evidence'
fi
printf '%s\n' \
'TC168-cache-inflight B33 QEMU PASS' \
'Exact KLD ownership, FBT-proven cache init/read calls, full-extent success/failure waiters, unmount drain, and cleanup exercised' \
'TC130 and full feature suite NOT_RUN'
+412
View File
@@ -0,0 +1,412 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
baseline=5cb420221ab0a4da7838c68db55280999474038b
makefile=$PRE15_DUT/src/Makefile
linux_makefile=$PRE15_ROOT/src-linux/Makefile
case_file=$PRE15_DUT/tests/pre15/cases/B34-build-groups.sh
artifacts=$PRE15_RUN_DIR/artifacts
for tool in git python3 sha256sum; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing B34 host tool: $tool"
done
pre15_record_fixture b34-makefile "$makefile"
pre15_record_fixture b34-linux-makefile "$linux_makefile"
pre15_record_fixture b34-case "$case_file"
mkdir -p "$artifacts"
pre15_target_reached
if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \
"$artifacts/B34-object-list.json" <<'PY'
from __future__ import annotations
from collections import Counter
from dataclasses import dataclass
from fnmatch import fnmatchcase
import json
from pathlib import Path
import re
import subprocess
import sys
root = Path(sys.argv[1])
dut = Path(sys.argv[2])
baseline = sys.argv[3]
report_path = Path(sys.argv[4])
makefile_path = dut / "src/Makefile"
def committed(path: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
raise SystemExit(f"cannot read B34 baseline {path}: {completed.stderr}")
return completed.stdout
def changed_paths() -> list[str]:
changed = subprocess.run(
[
"git",
"-C",
str(root),
"diff",
"--name-only",
baseline,
"--",
"repo-pre-15",
],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()
changed.extend(
subprocess.run(
[
"git",
"-C",
str(root),
"ls-files",
"--others",
"--exclude-standard",
"--",
"repo-pre-15",
],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()
)
return changed
expected_changed = [
"repo-pre-15/src/Makefile",
"repo-pre-15/tests/pre15/cases/B34-build-groups.sh",
]
changed = changed_paths()
if sorted(changed) != expected_changed or len(changed) != len(expected_changed):
raise SystemExit(f"B34 repo-pre-15 write set mismatch: {changed!r}")
@dataclass
class Evaluation:
variables: dict[str, str]
includes: list[str]
src_operations: list[tuple[str, list[str]]]
class MakeError(Exception):
pass
def logical_lines(source: str) -> list[tuple[int, str]]:
result: list[tuple[int, str]] = []
parts: list[str] = []
start_line = 0
for line_number, raw_line in enumerate(source.splitlines(), 1):
content = raw_line.split("#", 1)[0].rstrip()
if not parts and not content.strip():
continue
if not parts:
start_line = line_number
continued = content.endswith("\\")
if continued:
content = content[:-1].rstrip()
parts.append(content.strip())
if not continued:
result.append((start_line, " ".join(part for part in parts if part)))
parts = []
if parts:
raise SystemExit(f"unterminated Makefile continuation at line {start_line}")
return result
def expand(value: str, variables: dict[str, str]) -> str:
pattern = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_.]*)\}")
previous = None
while value != previous:
previous = value
value = pattern.sub(lambda match: variables.get(match.group(1), ""), value)
return value
def condition_value(expression: str, variables: dict[str, str], line: int) -> bool:
terms = [term.strip() for term in expression.split("&&")]
values: list[bool] = []
for term in terms:
empty_match = re.fullmatch(
r"empty\(([A-Za-z_][A-Za-z0-9_.]*):M([^()]*)\)", term
)
if empty_match:
variable, pattern = empty_match.groups()
words = variables.get(variable, "").split()
values.append(not any(fnmatchcase(word, pattern) for word in words))
continue
compare_match = re.fullmatch(
r'\$\{([A-Za-z_][A-Za-z0-9_.]*)\}\s*(==|!=)\s*"?([^" ]+)"?',
term,
)
if compare_match:
variable, operator, expected = compare_match.groups()
equal = variables.get(variable, "") == expected
values.append(equal if operator == "==" else not equal)
continue
raise SystemExit(f"unsupported Makefile condition at line {line}: {term}")
return all(values)
def evaluate(source: str, initial: dict[str, str]) -> Evaluation:
variables = dict(initial)
includes: list[str] = []
src_operations: list[tuple[str, list[str]]] = []
active_stack = [True]
for line_number, line in logical_lines(source):
if line.startswith(".if "):
parent_active = active_stack[-1]
active_stack.append(
parent_active
and condition_value(line.removeprefix(".if "), variables, line_number)
)
continue
if line == ".endif":
if len(active_stack) == 1:
raise SystemExit(f"unmatched .endif at line {line_number}")
active_stack.pop()
continue
if not active_stack[-1]:
continue
if line.startswith(".error "):
raise MakeError(line.removeprefix(".error "))
if line.startswith(".include "):
includes.append(line.removeprefix(".include ").strip())
continue
if line.startswith("."):
raise SystemExit(f"unsupported Makefile directive at line {line_number}: {line}")
assignment = re.fullmatch(
r"([A-Za-z_][A-Za-z0-9_.]*)\s*(\?=|\+=|=)\s*(.*)", line
)
if assignment is None:
raise SystemExit(f"unsupported Makefile statement at line {line_number}: {line}")
variable, operator, raw_value = assignment.groups()
value = expand(raw_value, variables)
if operator == "?=" and variable in variables:
continue
if operator == "+=":
old_value = variables.get(variable, "")
variables[variable] = " ".join(part for part in (old_value, value) if part)
else:
variables[variable] = value
if variable == "SRCS":
src_operations.append((operator, value.split()))
if len(active_stack) != 1:
raise SystemExit("unterminated Makefile condition")
return Evaluation(variables, includes, src_operations)
def evaluate_error(source: str, initial: dict[str, str]) -> str:
try:
evaluate(source, initial)
except MakeError as error:
return str(error)
raise SystemExit(f"Makefile unexpectedly accepted variables: {initial!r}")
def multiset_list(items: Counter[str]) -> list[str]:
return sorted(items.elements())
def object_multiset(sources: list[str]) -> Counter[str]:
objects: Counter[str] = Counter()
for source in sources:
if source.endswith((".c", ".cc", ".cpp", ".S", ".s")):
objects[str(Path(source).with_suffix(".o"))] += 1
return objects
before = committed("src/Makefile")
current = makefile_path.read_text(encoding="utf-8")
linux = (root / "src-linux/Makefile").read_text(encoding="utf-8")
expected_groups = [
(
"=",
[
"super.c",
"inode.c",
"data.c",
"namei.c",
"dir.c",
"erofs_vnops.c",
"vnode_if.h",
],
),
("+=", ["xattr.c"]),
("+=", ["decompressor.c", "zmap.c", "zdata.c"]),
(
"+=",
[
"decompressor_lz4.c",
"decompressor_lzma.c",
"decompressor_deflate.c",
"decompressor_zstd.c",
],
),
]
scenario_reports: dict[str, object] = {}
for config in ("0", "1"):
initial = {
"MACHINE_ARCH": "amd64",
"SYSDIR": "/freebsd/sys",
"WITH_ZSTDIO": config,
}
before_eval = evaluate(before, initial)
current_eval = evaluate(current, initial)
before_sources = before_eval.variables.get("SRCS", "").split()
current_sources = current_eval.variables.get("SRCS", "").split()
before_source_multiset = Counter(before_sources)
current_source_multiset = Counter(current_sources)
before_objects = object_multiset(before_sources)
current_objects = object_multiset(current_sources)
before_metadata = Counter(source for source in before_sources if not source.endswith(".c"))
current_metadata = Counter(source for source in current_sources if not source.endswith(".c"))
if current_eval.src_operations != expected_groups:
raise SystemExit(
f"B34 semantic SRCS groups differ for zstdio{config}: "
f"{current_eval.src_operations!r}"
)
if current_source_multiset != before_source_multiset:
raise SystemExit(f"B34 SRCS multiset changed for zstdio{config}")
if current_objects != before_objects:
raise SystemExit(f"B34 object multiset changed for zstdio{config}")
if current_metadata != before_metadata or current_metadata != Counter({"vnode_if.h": 1}):
raise SystemExit(f"B34 vnode_if handling changed for zstdio{config}")
if current_eval.includes != before_eval.includes or current_eval.includes != ["<bsd.kmod.mk>"]:
raise SystemExit(f"B34 bsd.kmod.mk ownership changed for zstdio{config}")
if (
current_eval.variables.get("KMOD") != before_eval.variables.get("KMOD")
or current_eval.variables.get("KMOD") != "erofs"
):
raise SystemExit(f"B34 KMOD ownership changed for zstdio{config}")
cflags_name = "CFLAGS.decompressor_zstd.c"
if current_eval.variables.get(cflags_name) != before_eval.variables.get(cflags_name):
raise SystemExit(f"B34 Zstd flags changed for zstdio{config}")
expected_flags = "-I/freebsd/sys/contrib/zstd/lib/freebsd"
if config == "1":
expected_flags += " -DZSTDIO"
if current_eval.variables.get(cflags_name) != expected_flags:
raise SystemExit(f"B34 ZSTDIO expansion changed for zstdio{config}")
scenario_reports[f"zstdio{config}"] = {
"sources": multiset_list(current_source_multiset),
"objects": multiset_list(current_objects),
"metadata": multiset_list(current_metadata),
"zstd_cflags": current_eval.variables[cflags_name].split(),
"includes": current_eval.includes,
}
default_initial = {"MACHINE_ARCH": "amd64", "SYSDIR": "/freebsd/sys"}
before_default = evaluate(before, default_initial)
current_default = evaluate(current, default_initial)
if before_default.variables.get("WITH_ZSTDIO") != "0":
raise SystemExit("B34 baseline default is not WITH_ZSTDIO=0")
if current_default.variables.get("WITH_ZSTDIO") != "0":
raise SystemExit("B34 changed the WITH_ZSTDIO default")
before_default_variables = dict(before_default.variables)
current_default_variables = dict(current_default.variables)
del before_default_variables["SRCS"]
del current_default_variables["SRCS"]
if current_default_variables != before_default_variables:
raise SystemExit("B34 changed a default-config Makefile variable expansion")
if current_default.includes != before_default.includes:
raise SystemExit("B34 changed a default-config Makefile include")
invalid_message = "WITH_ZSTDIO must be 0 or 1"
for source, label in ((before, "baseline"), (current, "current")):
message = evaluate_error(
source,
{"MACHINE_ARCH": "amd64", "SYSDIR": "/freebsd/sys", "WITH_ZSTDIO": "2"},
)
if message != invalid_message:
raise SystemExit(f"B34 {label} invalid-value contract drifted: {message!r}")
arch_message = "erofs supports only MACHINE_ARCH=amd64"
for config in ("0", "1"):
initial = {
"MACHINE_ARCH": "arm64",
"SYSDIR": "/freebsd/sys",
"WITH_ZSTDIO": config,
}
before_message = evaluate_error(before, initial)
current_message = evaluate_error(current, initial)
if before_message != arch_message or current_message != arch_message:
raise SystemExit(f"B34 amd64 gate changed for zstdio{config}")
for forbidden in ("CONFIG_", "obj-", "erofs-objs", "stub"):
if forbidden in current:
raise SystemExit(f"B34 introduced forbidden Linux/config stub surface: {forbidden}")
linux_categories = {
"core": "erofs-objs :=" in linux,
"xattr": "CONFIG_EROFS_FS_XATTR" in linux,
"compression": "CONFIG_EROFS_FS_ZIP" in linux,
"algorithms": all(
token in linux
for token in (
"CONFIG_EROFS_FS_ZIP_LZMA",
"CONFIG_EROFS_FS_ZIP_DEFLATE",
"CONFIG_EROFS_FS_ZIP_ZSTD",
)
),
}
if not all(linux_categories.values()):
raise SystemExit(f"B34 Linux semantic grouping reference drifted: {linux_categories!r}")
report = {
"baseline": baseline,
"write_set": changed,
"groups": [
{"operator": operator, "sources": sources}
for operator, sources in expected_groups
],
"configurations": scenario_reports,
"default_with_zstdio": current_default.variables["WITH_ZSTDIO"],
"invalid_with_zstdio_error": invalid_message,
"non_amd64_error": arch_message,
"linux_reference_categories": linux_categories,
}
report_path.write_text(
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii"
)
for config in ("zstdio0", "zstdio1"):
details = scenario_reports[config]
print(f"B34 {config} SRCS multiset: {' '.join(details['sources'])}")
print(f"B34 {config} object multiset: {' '.join(details['objects'])}")
print(f"B34 {config} Zstd flags: {' '.join(details['zstd_cflags'])}")
print("B34 default/invalid/amd64 gates: PASS")
print("B34 bsd.kmod.mk and vnode_if ownership: PASS")
print("B34 object-list equivalence: PASS")
PY
then
:
else
pre15_dut_fail 'B34 object-list or conditional expansion audit failed'
fi
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
PRE15_SMOKE_CODEC=lz4
export PRE15_SMOKE_CODEC
exec /bin/sh "$PRE15_DUT/tests/pre15/cases/SMOKE-common.sh"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
PRE15_SMOKE_CODEC=lzma
export PRE15_SMOKE_CODEC
exec /bin/sh "$PRE15_DUT/tests/pre15/cases/SMOKE-common.sh"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
PRE15_SMOKE_CODEC=plain
export PRE15_SMOKE_CODEC
exec /bin/sh "$PRE15_DUT/tests/pre15/cases/SMOKE-common.sh"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
PRE15_SMOKE_CODEC=zstd
export PRE15_SMOKE_CODEC
exec /bin/sh "$PRE15_DUT/tests/pre15/cases/SMOKE-common.sh"
+198
View File
@@ -0,0 +1,198 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
: "${PRE15_SMOKE_CODEC:?PRE15_SMOKE_CODEC is required}"
. "$PRE15_LIB_DIR/runner.sh"
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
generator=$fixture_dir/SMOKE-generate.py
kldsym_probe=$fixture_dir/SMOKE-kldsym.c
kld_builder=$fixture_dir/B28-build-kld.sh
artifacts=$PRE15_RUN_DIR/artifacts
source_first=$PRE15_CASE_TMP/source-first
source_second=$PRE15_CASE_TMP/source-second
image=$PRE15_CASE_TMP/SMOKE-$PRE15_SMOKE_CODEC.erofs
module=$PRE15_CASE_TMP/SMOKE-$PRE15_SMOKE_CODEC-erofs.ko
case "$PRE15_SMOKE_CODEC" in
plain)
config=0
uuid=00000000-0000-4000-8000-000000000100
mkfs_codec=
case_timeout=180
;;
lz4)
config=0
uuid=00000000-0000-4000-8000-000000000104
mkfs_codec='-zlz4 -C4096'
case_timeout=240
;;
lzma)
config=0
uuid=00000000-0000-4000-8000-0000000001a0
mkfs_codec='-zlzma,level=6,dictsize=65536 -C4096'
case_timeout=300
;;
zstd)
config=1
uuid=00000000-0000-4000-8000-00000000025d
mkfs_codec='-zzstd,level=3,dictsize=65536 -C4096'
case_timeout=300
;;
*) pre15_runner_fail "unknown smoke codec: $PRE15_SMOKE_CODEC" ;;
esac
for tool in clang cmp diff file mkfs.erofs nm python3 scp sha256sum timeout; do
command -v "$tool" >/dev/null 2>&1 || \
pre15_infra_blocked "missing smoke tool: $tool"
done
for fixture in "$generator" "$kldsym_probe" "$kld_builder" \
"$PRE15_DUT/tests/pre15/cases/SMOKE-common.sh"; do
pre15_record_fixture "smoke-$(basename "$fixture")" "$fixture"
done
mkdir -p "$artifacts"
if ! python3 -B "$generator" --output "$source_first" \
>"$artifacts/source-first.json" 2>"$artifacts/source-first.stderr" || \
! python3 -B "$generator" --output "$source_second" \
>"$artifacts/source-second.json" 2>"$artifacts/source-second.stderr"; then
pre15_runner_fail 'smoke fixture generation failed'
fi
if ! diff -qr "$source_first" "$source_second" \
>"$artifacts/source-repeat.diff" || \
! cmp "$artifacts/source-first.json" "$artifacts/source-second.json"; then
pre15_runner_fail 'smoke source fixture is not byte reproducible'
fi
if ! timeout -k 5 120 sh -c \
"mkfs.erofs -d0 -T0 --all-time --all-root --workers=1 --sort=path -x-1 -U$uuid $mkfs_codec '$image' '$source_first'" \
>"$artifacts/mkfs.stdout" 2>"$artifacts/mkfs.stderr"; then
pre15_runner_fail "$PRE15_SMOKE_CODEC smoke image generation failed"
fi
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
"$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" "$config" \
>"$artifacts/kld-build.stdout" 2>"$artifacts/kld-build.stderr"; then
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke cross-target KLD build failed"
fi
pre15_record_module "$module"
sha256sum "$image" >"$artifacts/image-sha256.txt"
sha256sum "$module" >"$artifacts/kld-sha256.txt"
file "$module" >"$artifacts/kld-file.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/kld-nm-u.txt"
pre15_target_reached
test "${PRE15_MODE:-}" = qemu || \
pre15_runner_fail 'smoke cases require QEMU mode'
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"
pre15_scp()
{
timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
-o BatchMode=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
}
remote_root=/root/pre15-smoke-$PRE15_SMOKE_CODEC
pre15_guest_ssh_bounded rm -rf "$remote_root"
pre15_guest_ssh_bounded mkdir -p "$remote_root"
if ! pre15_scp "$image" "$remote_root/image.erofs" || \
! pre15_scp "$module" "$remote_root/erofs.ko" || \
! pre15_scp "$kldsym_probe" "$remote_root/SMOKE-kldsym.c"; then
pre15_infra_blocked "$PRE15_SMOKE_CODEC smoke transfer failed"
fi
if test "$PRE15_SMOKE_CODEC" = zstd; then
if ! pre15_guest_ssh_bounded \
"cc -O2 -Wall -Wextra -Werror -std=c17 '$remote_root/SMOKE-kldsym.c' -o '$remote_root/SMOKE-kldsym'"; then
pre15_infra_blocked 'could not compile guest ZSTDIO capability probe'
fi
if ! pre15_guest_ssh_bounded "$remote_root/SMOKE-kldsym" \
ZSTD_DCtx_setParameter ZSTD_createDCtx_advanced \
ZSTD_decompressStream ZSTD_freeDCtx ZSTD_isError \
>"$artifacts/zstdio-capability.txt" 2>"$artifacts/zstdio-capability.stderr"; then
pre15_infra_blocked 'running guest kernel lacks required ZSTDIO symbols'
fi
fi
if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
if ! pre15_guest_ssh_bounded kldload "$remote_root/erofs.ko" \
>"$artifacts/kldload.stdout" 2>"$artifacts/kldload.stderr"; then
if grep -q 'module already loaded or in kernel' \
"$artifacts/kldload.stderr"; then
pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
if test "$PRE15_SMOKE_CODEC" = zstd && \
grep -Eqi 'link_elf|symbol|not defined' "$artifacts/kldload.stderr"; then
pre15_infra_blocked 'zstdio1 EROFS KLD cannot load on this guest kernel'
fi
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke KLD failed to load"
fi
pre15_own_guest_kld erofs "$PRE15_SMOKE_CODEC smoke KLD"
smoke_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
-f "$remote_root/image.erofs") || \
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke md attach failed"
case "$smoke_md" in
md[0-9]*) ;;
*) pre15_runner_fail "unexpected smoke md unit: $smoke_md" ;;
esac
pre15_own_guest_md "$smoke_md" "$PRE15_SMOKE_CODEC smoke md"
smoke_mount=/mnt/pre15-smoke-$PRE15_SMOKE_CODEC
pre15_guest_ssh_bounded mkdir -p "$smoke_mount"
if ! pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$smoke_md" "$smoke_mount"; then
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke readonly mount failed"
fi
pre15_own_guest_mount "$smoke_mount" "$PRE15_SMOKE_CODEC smoke mount"
python3 -B - "$artifacts/source-first.json" "$artifacts/expected-files.tsv" <<'PY'
import json
from pathlib import Path
import sys
manifest = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
lines = [f"{item['path']}\t{item['size']}\t{item['sha256']}" for item in manifest["files"]]
Path(sys.argv[2]).write_text("\n".join(lines) + "\n", encoding="ascii")
PY
while IFS="$(printf '\t')" read -r path expected_size expected_hash; do
actual_size=$(pre15_guest_ssh_bounded stat -f %z "$smoke_mount/$path") || \
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke stat failed: $path"
actual_hash=$(pre15_guest_ssh_bounded sha256 -q "$smoke_mount/$path") || \
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke hash failed: $path"
printf '%s\t%s\t%s\n' "$path" "$actual_size" "$actual_hash" \
>>"$artifacts/guest-files.tsv"
test "$actual_size" = "$expected_size" && test "$actual_hash" = "$expected_hash" || \
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke file mismatch: $path"
done <"$artifacts/expected-files.tsv"
pre15_guest_ssh_bounded \
"find '$smoke_mount' -mindepth 1 -maxdepth 1 -exec basename '{}' ';' | sort" \
>"$artifacts/guest-root-entries.txt"
printf '%s\n' empty nested payload.bin >"$artifacts/expected-root-entries.txt"
cmp "$artifacts/expected-root-entries.txt" "$artifacts/guest-root-entries.txt" || \
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke root readdir set mismatch"
if test "$PRE15_SMOKE_CODEC" = lz4; then
dd if="$source_first/payload.bin" bs=1 skip=4093 count=131071 2>/dev/null | \
sha256sum | awk '{print $1}' >"$artifacts/expected-range-sha256.txt"
pre15_guest_ssh_bounded \
"dd if='$smoke_mount/payload.bin' bs=1 skip=4093 count=131071 2>/dev/null | sha256 -q" \
>"$artifacts/guest-range-sha256.txt"
cmp "$artifacts/expected-range-sha256.txt" "$artifacts/guest-range-sha256.txt" || \
pre15_dut_fail 'LZ4 smoke cross-pcluster partial range mismatch'
fi
pre15_guest_ssh_bounded dmesg >"$artifacts/guest-dmesg.txt"
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free' \
"$artifacts/guest-dmesg.txt"; then
pre15_dut_fail "$PRE15_SMOKE_CODEC smoke observed kernel failure evidence"
fi
printf '%s\n' \
"SMOKE-$PRE15_SMOKE_CODEC PASS mode=zstdio$config deadline=$case_timeout" \
'readonly mount, exact file hashes/sizes, readdir set, and owned cleanup exercised'
+85
View File
@@ -0,0 +1,85 @@
#!/bin/sh
set -eu
: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"
spec=$PRE15_DUT/tests/pre15/fixtures/B01-xattr-legacy.json
helper=$PRE15_DUT/tests/pre15/fixtures/g3.py
output=$PRE15_CASE_TMP/xattr-legacy
pre15_record_fixture xattr-legacy-contract "$spec"
pre15_record_fixture xattr-legacy-generator "$helper"
if python3 -B "$helper" make-xattr-legacy \
--spec "$spec" --output "$output" \
--freebsd-root "$PRE15_DUT/src" --linux-root "$PRE15_ROOT/src-linux"; then
:
else
pre15_runner_fail 'legacy xattr fixture generation or independent parsing failed'
fi
mkdir -p "$PRE15_RUN_DIR/artifacts"
cp "$output/fixture-manifest.json" \
"$PRE15_RUN_DIR/artifacts/TC162-fixture-manifest.json"
pre15_record_fixture xattr-legacy-manifest \
"$PRE15_RUN_DIR/artifacts/TC162-fixture-manifest.json"
python3 - "$PRE15_RUN_DIR/artifacts/TC162-fixture-manifest.json" <<'PY'
import json
from pathlib import Path
import sys
manifest = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
positive = {
"legacy-primary": "primary-legacy",
"explicit-plain": "primary",
"packed-carrier": "packed",
"metabox-carrier": "metabox",
}
for name, carrier in positive.items():
item = manifest["results"][name]
if item["carrier"] != carrier or not item["checksum_valid"]:
raise SystemExit(f"{name}: carrier/checksum oracle failed")
if item["base_index"] != 1 or item["infix"] != "repo.pre15.legacy.":
raise SystemExit(f"{name}: prefix value differs")
for name in ("legacy-length-invalid", "legacy-offset-outside"):
item = manifest["results"][name]
if item["expected_errno"] != "EINTEGRITY" or not item["checksum_valid"]:
raise SystemExit(f"{name}: exact negative oracle failed")
print("TC162: legacy, explicit plain, packed, and metabox carriers read one value")
print("TC162: single-field negatives return exactly EINTEGRITY")
PY
pre15_target_reached
if python3 - "$PRE15_DUT/src/xattr.c" "$PRE15_ROOT/src-linux/xattr.c" <<'PY'
from pathlib import Path
import sys
freebsd = Path(sys.argv[1]).read_text(encoding="utf-8")
linux = Path(sys.argv[2]).read_text(encoding="utf-8")
freebsd_markers = (
"prefix_en = NULL;",
"EROFS_FEATURE_COMPAT_PLAIN_XATTR_PFX",
"else if (em->packed_inode != NULL)",
"else if (em->packed_nid != 0)",
"erofs_xattr_read_metadata(em, prefix_en",
)
linux_markers = (
"bool plain = erofs_sb_has_plain_xattr_pfx(sbi);",
"else if (sbi->packed_inode)",
"else\n\t\t\tplain = true;",
"if (plain)\n\t\t(void)erofs_init_metabuf(&buf, sb, false);",
)
if not all(marker in freebsd for marker in freebsd_markers):
raise SystemExit("FreeBSD legacy primary fallback changed")
if not all(marker in linux for marker in linux_markers):
raise SystemExit("Linux legacy primary fallback changed")
print("TC162: FreeBSD and Linux retain the no-carrier primary fallback")
PY
then
:
else
pre15_dut_fail 'legacy xattr primary fallback branch no longer matches the frozen contract'
fi