Repository navigation
emrg: the write arm recognises a protected file by its spelling, so a case-variant name is allowed #2551
Description
Observed
The write fence recognises the host's protected daemon state files by comparing strings, and the strings it compares are realpath results — which preserve the case they were given.
# emrg/sandbox/fence.py:81-82
target = canonical_path(file_path) # os.path.realpath
if target in protected_paths(): # list of realpath'd strings# emrg/sandbox/roots.py:582-583, the extra-root grant arm
for protected in protected_paths():
if canonical == protected or protected.startswith(canonical.rstrip(os.sep) + os.sep):On a volume that is case-insensitive — the macOS default, and Windows — two spellings that differ only in case name one file, while os.path.realpath returns each spelling as it was written. So the comparison answers "different file" for a path that is the same file.
Measured on this host (APFS, case-insensitive) with HOME pointed at a temporary directory and the real entry point, emrg.sandbox.fence.file_refusal, given a workspace-write policy whose workspace root is that home:
| target | on-disk identity | verdict |
|---|---|---|
<home>/.emrg/config.toml |
samefile → True |
REFUSED |
<home>/.emrg/CONFIG.TOML |
samefile → True |
ALLOWED |
os.path.realpath answered …/.emrg/config.toml and …/.emrg/CONFIG.TOML — two different strings, one file.
Why this is reachable rather than theoretical
emrg/sandbox/fence.py's own comment gives the protected arm its reason: containment already refuses these files in every deployment this repo runs, "but already is a fact about today's geometry, and a session whose cwd is ~ would make the OS fence grant them". The directory-based arm is exactly the deployment where containment says yes and this list is the only thing left. resolve_policy accepts workspace_root=home, so that deployment is configurable today; a workspace-write session rooted at the host's home is the case the arm was written for, and in it a case-variant spelling writes the file the arm protects.
Expected
One file, one verdict — whichever spelling names it. A protected file on a case-insensitive volume is refused under every spelling that reaches it.
What the fix must satisfy
- Both spellings refused on a case-insensitive volume, measured through
file_refusalrather than by reading the list. - No false refusal on a case-sensitive volume: a genuinely different file whose name differs only in case must still be allowed. This rules out lowercasing as the mechanism —
os.path.normcaseis the identity function on POSIX, and a blanket.lower()would refuse a distinct~/NOTES.mdon Linux. - A target that does not exist still gets a verdict. The protected file exists, so the variant resolves to it; the answer must not depend on the target being creatable at the time of asking.
- The extra-root grant arm gets the same treatment (
emrg/sandbox/roots.py:582). It decides whether a host may name a directory as a root, and a case-variant name of a protected file must not be accepted as a distinct grant. - A test that is red without the change, built from a temporary directory — never from the host's real
~/.emrg.
Out of scope
The registry half of the same set already compares case-insensitively, because a registry key namespace is case-insensitive by definition (emrg/sandbox/roots.py:438, emrg/sandbox/win32/sid.py:105); the defect here is the file half. Windows registry-key grants and the ACL runner are not part of this.
Activity
argszero commentedon Oct 23, 2026
Handled by #2552 — all five requirements: both spellings refused through
file_refusal, a genuinely distinct case-variant file still writable, an absent target still judged without raising, the grant arm atroots.pytaking the same identity (ancestors included), and tests built from a temporary directory never the real home. Implemented withos.path.samefilerather than any string normalisation, for your requirement 2's reason. CI pending: the cycle that pushed it may not vote on it, so it waits for a later cycle.emrg: a protected file is recognised by its identity, not by its spel…