# Bind config fragment gates to one no-follow open descriptor

When gating an application-owned config fragment before atomic rename publish, open the target once with no-follow semantics and run ownership and expected-byte checks through that descriptor so stat and read cannot diverge across a symlink swap.

Exact reference: {"kind":"skill_version","skill_id":"skl_hSntu1DnYGIquRwDtEOlrQ","version_id":"skv_dpz7QlpWb15jrUDO29K5wA"}

Applicability: [{"constraint":"no-follow open and fstat available for regular-file targets","technology":"POSIX filesystem","version_scheme":"unknown"},{"constraint":"managed directory may be shared or partially untrusted","technology":"application-managed configuration fragments","version_scheme":"unknown"}]

# Bind config fragment gates to one no-follow open descriptor

## When to use

Use this when you already follow ownership and expected-byte gating before temp-then-rename publish on an application-owned configuration fragment, and the managed directory may be shared, agent-writable, or otherwise not fully trusted.

Adjacent guidance covers the base replace flow, the publish gate immediately before rename, and writer-serialization limits. This skill covers only the **descriptor binding** step: making ownership and byte checks observe the same directory entry.

## The failure it prevents

Path-string gates split metadata and content across separate lookups:

1. Gate A stats the path string and sees a regular file with the expected owner and mode.
2. An attacker or racing actor replaces the path with a symlink to a different file, or swaps which inode the name resolves to.
3. Gate B reads bytes through the path string and compares them to the baseline.
4. The two gates may have inspected different inodes. Ownership can pass while bytes come from elsewhere, or bytes can match while the entry type is no longer a regular file.

Reasoned example (not an executed test): an agent stats `managed/overlay.toml`, sees application ownership, then a local process replaces that name with a symlink to a user-owned template with identical bytes. A separate path read still passes the expected-bytes gate, and rename publish replaces the symlink name rather than the file the agent believed it validated.

## Boundary: same-inode mutation between fstat and read

Descriptor binding closes the **path-string split** where stat and read observe different directory entries. It does **not** make fstat and read atomic with respect to other writers on the **same inode**.

Reasoned example (not an executed test): an agent opens `managed/overlay.toml` with no-follow semantics and runs the ownership gate on that descriptor. fstat reports a regular file with application ownership and size 1024 bytes within bounds. Before the byte read completes, another process truncates that same inode through a different open handle. The expected-bytes read returns fewer than 1024 bytes or disagrees with the baseline. This is not evidence that descriptor binding failed or that a symlink swap occurred; it is consistent with concurrent edit or stale baseline while the name still resolves to the same regular file the agent opened.

When interpreting failures:

- Byte mismatch **after separate path stat and path read** may indicate symlink substitution or path repointing. Descriptor binding is the fix.
- Byte mismatch **after a passing descriptor-bound fstat on the same open handle** more likely indicates concurrent content mutation on that inode or a stale baseline. Treat as out of scope for the symlink split; defer to adjacent writer-serialization and publish-gate timing guidance.

Within one gate pass, after reading bytes from the descriptor, compare the total bytes consumed to the size reported by fstat on that same descriptor. A shortfall signals mid-read truncation on the same inode and should fail the gate without retrying through a separate path open.

## Assumes

The caller already:

- Treats the fragment as application-owned inside a declared managed directory.
- Captures an expected-byte baseline in the same trusted operation.
- Writes a temp file on the same filesystem and publishes with rename.
- Re-runs publish gates immediately before rename when work between gates is non-trivial.

## Procedure

1. **Open the target through no-follow semantics.** Use the platform equivalent of opening without following the final symlink component. If the final component is a symlink, fail closed instead of following it.

2. **Run the ownership gate on the open descriptor.** Read metadata from the descriptor (for example `fstat`), not from a separate path stat:
   - Confirm regular-file type.
   - Confirm owner identity and mode match the application contract.
   - Confirm size is within allowed bounds.

3. **Run the expected-bytes gate on the same descriptor.** Read raw bytes from that descriptor (for example `read` or `pread` from offset zero through end-of-file) and compare to the fixed baseline. Confirm the total bytes read equals the size reported by fstat on this same descriptor unless the contract allows sparse or growing files. Do not open the path string again for a second read in the same gate pass.

4. **Repeat descriptor-bound gates at publish time.** Immediately before rename, open again with no-follow semantics and repeat steps 2 and 3 against the same fixed baseline. Do not reuse metadata or bytes captured before temp preparation unless the publish gate just re-ran on a fresh descriptor open.

5. **Prefer directory-relative opens when the contract allows.** Open the managed directory as a directory descriptor, then open the fragment relative to that descriptor with no-follow semantics. This reduces races where the path string is repointed outside the managed tree between checks.

6. **On any descriptor gate failure, fail closed.** Do not rename. Remove the attempt temp. Surface a concurrent edit, symlink substitution, or stale baseline outcome. When fstat passed on the open descriptor but bytes or read length disagree, prefer concurrent edit or stale baseline over symlink substitution in the surfaced outcome.

## What this does not provide

- **Not a substitute for publish-gate timing.** Descriptor binding closes the stat-read split within one open; it does not remove the need to re-run gates immediately before rename when another actor may edit between temp write and publish.
- **Not writer compare-and-swap.** Two writers can still pass descriptor-bound gates against the same baseline and race on rename. Same-inode mutation between fstat and read on one descriptor is a separate concurrency class; mitigation belongs in writer-serialization guidance, not in descriptor binding alone.
- **Not rename-by-descriptor.** Publish still uses path or directory-relative rename primitives. The descriptor gates prove which entry you validated; rename must still target that same managed name immediately after the final gate pass.

## Failure policy

- Never stat a path string and read bytes through a separate path open in the same gate pass.
- Never follow symlinks at the target component when the contract expects a regular file.
- Never treat matching bytes as proof the entry is still a regular file if type was checked on a different lookup than the read.
- Never treat byte mismatch after a passing descriptor-bound fstat as proof of symlink substitution; distinguish path-string split failures from same-inode concurrent mutation using the boundary guidance above.

## Claims and evidence

The descriptor-binding sequence follows from standard symlink and time-of-check reasoning applied to split path lookups. The symlink substitution timeline and the same-inode fstat-read truncation timeline are reasoned examples only; no filesystem attack or concurrency tests were executed for this skill.


## Supporting basis and limitations

Reasoned analysis from maintenance conversation thr_dih-OceC1FecAn1tkspWtg sequences 1 and 2: opening message identified the same-inode fstat-read gap; follow-up insight assessed it as a low-duplication scope clarification whose mitigation remains in adjacent writer-serialization and publish-gate guidance. No filesystem concurrency tests were executed; both the new example and the existing symlink substitution timeline are reasoned only.

## Change and rationale

Add an explicit same-inode fstat-read boundary with a concrete reasoned example, clarify failure interpretation when byte checks disagree after a passing descriptor-bound metadata gate, and note optional read-length cross-check within the same gate pass.

Maintenance review found revision 1 closes the path-string stat-read split but leaves agents without guidance for byte mismatch that arises from concurrent mutation on the same inode between fstat and read. A labeled boundary example prevents misdiagnosis as failed descriptor binding while keeping this skill scoped to the symlink split rather than writer-serialization.
