Skip to content

audit-enforceability: the stub-home fence's case fold does not cover Unicode normalization, leaving the escape open on APFS and HFS+ #3934

Closed

Description

@kyle-sexton

Summary

PR #3927 closed the absent-ancestor hole in the stub-home fence by adding may_be_within, whose first arm is a spelling fold that consults no locale. That fold covers CASE differences. It does not cover Unicode NORMALIZATION, and APFS and HFS+ fold normalization while NTFS and ext4 do not.

The comments shipped in #3927 disclose this accurately at plugins/review/skills/audit-enforceability/scripts/emit-stubs.sh:51-53 and :319-321: the fold is "coarser than the CASE fold of any filesystem this runs on ... it is not coarser than a Unicode NORMALIZATION fold, which APFS and HFS+ apply and NTFS does not". Disclosure is not closure, so this issue tracks the remaining hole.

The hole

fold_path maps ASCII letters to upper case and every run of anything else to a single placeholder. For a precomposed é (NFC, U+00E9) it yields one placeholder; for a decomposed e plus combining acute (NFD, U+0065 U+0301) it yields the ASCII E followed by a placeholder. Those are different strings, so the fold reports two directories.

On APFS and HFS+ they are one directory. When the ancestor does not yet exist, the -ef arm cannot settle the question either, because there is nothing on disk to compare device and inode against. That is precisely the condition #3925 described, so the original escape is reachable again on those filesystems: stubs land inside the declared --scan-dir at exit 0.

Reproduction, on macOS with a case-insensitive normalizing volume, both directories absent beforehand:

T=$(mktemp -d); M="$T/mem"; mkdir -p "$M"
S=plugins/review/skills/audit-enforceability
# NFC in one path, NFD in the other, same directory to the filesystem
bash $S/scripts/emit-stubs.sh \
  --findings $S/evals/fixtures/findings-one-per-rung.md \
  --classes  $S/evals/fixtures/classification-one-per-rung.tsv \
  --out      "$M/réviews/feat-x" \
  --scan-dir "$M/réviews/feat-x" \
  --memory-root "$M"

with the two é characters written in opposite normalization forms.

This was NOT executed. Both hosts available during verification are byte-exact: NTFS folds none of the ten Unicode pairs tried, and ext4 folds nothing. The finding is derived from the fold's own definition plus the documented behaviour of APFS and HFS+, and the shipped comments now state the same thing. It is reported as unexecuted rather than as observed.

Why it matters here

The repository recognizes macOS as a target platform: scripts/check-shell-portability.sh:131. As with the original escape, the load-bearing exclusion still holds even when the fence is bypassed, because a stub declares type: enforceability-stub and the fix pass admits only type: review-findings. This is a defence-in-depth gap, not a live corruption of the fix pass, and it is unreachable through the skill's own home composition, where both homes derive from the same charset-sanitized slug and the fixed literals enforceability and reviews. It is reachable at ladder rungs 2 through 4, where a consumer supplies the two homes independently.

Acceptance criteria

  • The fence refuses an NFC versus NFD spelling pair of the same directory when that directory does not exist, on a normalizing filesystem, at the same exit code it uses when the directory does exist.
  • The fix does not introduce over-refusal on byte-exact filesystems: two genuinely distinct directories whose names differ only by normalization must still be usable on NTFS and ext4, where the filesystem itself treats them as distinct.
  • A test case covers the normalization pair. If it cannot execute on the CI runners available, it is skipped with a stated reason naming the filesystem property it needs, in the same honest style as the existing case 16 skip, rather than silently omitted.
  • The comments at emit-stubs.sh:51-53 and :319-321 are updated to describe whatever the fix actually covers.
  • plugins/review is bumped past its published version with a matching new changelog entry.

Context

Raised by the independent verifier that re-attacked PR #3927 and cleared it as SAFE TO MERGE. It is recorded as the one residual that verdict does not cover, and it is pre-existing relative to that PR: #3927 narrowed the hole from every filesystem to normalizing filesystems only, and disclosed the remainder rather than leaving it unstated.

Refs #3925, #3927, #3800

Activity

  1. added
    priority: mediumReal value, no hard deadline; normal backlog flow.
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    on Sep 7, 2026
  2. added
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    and removed
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    on Sep 27, 2026
  3. kyle-sexton commented on Sep 29, 2026

    @kyle-sexton
    MemberAuthor

    Audit note. This comes from the 2026-09-29 audit of the Cursor agent run.

    Which change delivered this. PR #5043 (review 0.32.1: NFC fold plus volume probe in emit-stubs.sh) delivered the fix. It used Refs, because this issue had already been closed by park PR #4964 (Closes #3934, 2026-09-28 05:26Z; the park ledger was reverted by #5044). The closure event therefore points at the wrong PR.

    What the audit found. The park was an agent choice on a needs-human fix request with no owner comment. The fix landed about 70 minutes later. The issue stays closed as COMPLETED; the row is ratify, so it is not reopened.

    What is proven and what is not.

    • Proven: the string-level fold, asserted by --check-normalization-fold.
    • Proven: byte-exact no-over-refusal on ext4 and NTFS.
    • Not proven: the exit-3 refusal arm never ran on a normalizing volume, because both CI hosts are byte-exact. Acceptance criterion 1 is unverified on real APFS or HFS+.

    Follow-up. PR #5277 makes skip_case in emit-stubs.test.sh name this gap and fixes stale comments.

    Optional owner check (macOS). On a macOS APFS host:

    1. Run the reproduction from the issue body: two absent paths, one spelled with NFC and one with NFD e-acute.
    2. Run emit-stubs.sh with --out, --scan-dir, and --memory-root set to those paths.
    3. Expect exit 3 and no directories created.
    4. Report the exit code on this issue.

    needs-human stays.

  4. kyle-sexton commented on Sep 29, 2026

    @kyle-sexton
    MemberAuthor

    Acceptance check of #5043 (merged as 00ca004c4, review 0.32.1) against this issue, from the tree at origin/main on 2026-09-29. Decision: the issue stays closed as completed; it is not reopened and not re-labelled not planned. The closure event points at park PR #4964 (ledger reverted in #5044), but the change that answers the issue is #5043.

    # Criterion Status Evidence
    1 Fence refuses an NFC vs NFD pair of an absent directory on a normalizing volume, same exit code as when it exists Implemented, not executed on a real volume emit-stubs.sh puts the absent tail in NFC after a same-device probe says the volume folds normalization (APFS, HFS+). The string-level fold is asserted by --check-normalization-fold. The exit-3 refusal arm has never run on APFS or HFS+ because both CI hosts are byte-exact.
    2 No over-refusal on byte-exact filesystems Met Case 31 keeps both spellings usable on ext4 and NTFS.
    3 Test covers the pair; skipped with a stated reason where it cannot run Met Case 31 in emit-stubs.test.sh, with skip_case naming the filesystem property. PR #5277 makes the skip text say the refusal arm is unexecuted and adds EMIT_STUBS_ASSUME_NORMALIZING=1 so the refusal arm runs on a byte-exact runner (a test of the code path, not of a real volume).
    4 Comments at the two cited sites describe what the fix covers Met emit-stubs.sh:46-66 and :370-378 describe the probe-gated NFC step; PR #5277 corrects the comment that says nothing is created (the probe makes and removes a private directory) and documents the probe cleanup trap.
    5 Version bump with changelog entry Met plugins/review 0.32.1 (#5043).

    Only criterion 1 has a gap, and it is a verification gap, not a code gap. needs-human stays for the optional macOS check: on an APFS host, run the reproduction from the issue body with two absent paths (one NFC, one NFD e-acute), expect exit 3 and no directories created, and report the exit code here. A non-3 exit is the condition to reopen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.priority: mediumReal value, no hard deadline; normal backlog flow.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions