#!/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 #include #include #include #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 \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'