diff --git a/changelog.d/8502-string-payload-ratchet.md b/changelog.d/8502-string-payload-ratchet.md new file mode 100644 index 0000000000..844198dadf --- /dev/null +++ b/changelog.d/8502-string-payload-ratchet.md @@ -0,0 +1 @@ +Tooling: harden the `StringHeader` payload-access ratchet against `byte_add` and signed-offset pointer arithmetic, and make its self-test exercise crate discovery, per-crate attribution, and stale-baseline failure end to end (#8429). diff --git a/scripts/string_payload_access_inventory.py b/scripts/string_payload_access_inventory.py index bed58eb101..f0056fc867 100755 --- a/scripts/string_payload_access_inventory.py +++ b/scripts/string_payload_access_inventory.py @@ -24,6 +24,7 @@ import argparse import re import sys +import tempfile from collections import Counter from dataclasses import dataclass from pathlib import Path @@ -33,9 +34,11 @@ RULES = ("inline-offset", "reader-helper") INLINE_OFFSET_RE = re.compile( - r"(?:\.(?:add|wrapping_add)\s*\(\s*|\+\s*)" + r"(?:\.(?:add|wrapping_add|byte_add|wrapping_byte_add|offset|wrapping_offset)" + r"\s*\(\s*|\+\s*)" r"(?:(?:std|core)::mem::)?size_of\s*::\s*<\s*" - r"(?:[A-Za-z_][A-Za-z0-9_]*::)*StringHeader\s*>\s*\(\s*\)", + r"(?:[A-Za-z_][A-Za-z0-9_]*::)*StringHeader\s*>\s*\(\s*\)" + r"(?:\s+as\s+(?:usize|isize))?", re.MULTILINE, ) FUNCTION_RE = re.compile(r"\bfn\s+([A-Za-z_][A-Za-z0-9_]*)[^;{]*\{", re.MULTILINE) @@ -311,6 +314,22 @@ def expect(condition: bool, message: str) -> None: "synthetic copy-pasted reader helper was not detected exactly once", ) + alternate_offsets = r''' +unsafe fn alternate_offsets(ptr: *const u8) { + let _ = ptr.byte_add(core::mem::size_of::()); + let _ = ptr.wrapping_byte_add(size_of::()); + let _ = ptr.offset(std::mem::size_of::() as isize); + let _ = ptr.wrapping_offset(size_of::() as isize); +} +''' + alternate_findings = scan_text( + "synthetic-crate", "crates/synthetic-crate/src/alternate.rs", alternate_offsets + ) + expect( + sum(f.rule == "inline-offset" for f in alternate_findings) == 4, + "alternate raw-pointer payload offsets were not all detected", + ) + clean = r''' fn sanctioned(ptr: *const perry_runtime::StringHeader) -> Vec { unsafe { perry_runtime::string::OwnedStringBytes::copy_from_header(ptr) } @@ -325,6 +344,35 @@ def expect(condition: bool, message: str) -> None: "sanctioned access, comments, or strings produced a finding", ) + # Exercise crate discovery and the ratchet end to end. This prevents the + # scanner's regex unit tests from staying green if workspace traversal or + # per-crate attribution is accidentally broken. + with tempfile.TemporaryDirectory() as temp_dir: + temp_root = Path(temp_dir) + crate_dir = temp_root / "crates" / "synthetic-crate" + source = crate_dir / "src" / "lib.rs" + source.parent.mkdir(parents=True) + (crate_dir / "Cargo.toml").write_text( + '[package]\nname = "synthetic-crate"\nversion = "0.0.0"\n', + encoding="utf-8", + ) + source.write_text(planted, encoding="utf-8") + discovered, files_scanned = collect_inventory(temp_root) + expect(files_scanned == 1, "synthetic crate source was not scanned exactly once") + expect( + counts_for(discovered) == counts_for(findings), + "filesystem inventory disagreed with direct source scanning", + ) + + planted_baseline = dict(counts_for(discovered)) + source.write_text(clean, encoding="utf-8") + removed, _ = collect_inventory(temp_root) + regressions, stale = compare_counts(counts_for(removed), planted_baseline) + expect( + not regressions and bool(stale), + "removing a planted offender did not make its baseline fail stale", + ) + actual = counts_for(findings) regressions, stale = compare_counts(actual, {}) expect(bool(regressions) and not stale, "zero baseline did not reject offenders")