# P15-019 Stage0 Decision

Status: `GO`. G03 authorizes B16 for P15-019 only. P15-046 remains the B01
test-only legacy xattr prefix fallback contract; it neither changes this
decision nor authorizes any exact-header or xattr behavior change.

The decision is bound to DUT BASE
`d645feb720c7022d2138d2a62eb72c022eb75351`, FreeBSD source HEAD
`106727738dcfb6c001b46f25363b91cece970085`, erofs-utils source HEAD
`7db78788b000999e2de88decd2ba90654f26171c`, and
`mkfs.erofs (erofs-utils) 1.8.6`.

## Authoritative Run

The gate ran from `/work/erofs-freebsd-pre/repo-pre-15` as:

```sh
umask 077
timeout -k 10 240 tests/pre15/gates/P15-019.sh \
    --base d645feb720c7022d2138d2a62eb72c022eb75351 \
    --output /work/erofs-freebsd-pre-evidence/pre15/\
20260815T093800Z-G03-P15-019-umask-fix/gate-output
```

It returned zero with `GO`. The authoritative hashes are:

- gate script: `afa20cbd28ca9dfc8064596a312248756ed7cde54398efb5ef713818cc04ea54`
- gate input: `291bf12838981ef650e16bbf7381eaa9b2921de957e97dec46e6a573750a19b8`
- result: `bb52e1a88cb09554a51432ec545ed98d5509a529832141ed0ce83b0c336be5bb`
- cases: `0c12cdce8b3ed37e85f349af0715a728d4acabbb758451fb65546dd9836066b5`
- fixture manifest/set: `5e99e5ad9112508991e78ae6e65928973d6b0da78285a059894bed06010cf373`
- semantics: `090e6b0cd5eaa197f17da12daf29f30bd8f337d3d4ae2f0f4f5819ad41e906f9`
- cleanup: `8e3b52610ee1492262494d00c48bf66ea04d313ab9ac1ef27565fa0e607b9a97`

An additional independent replay from `umask 077` produced the same result,
cases, fixture manifest, semantics, and cleanup hashes byte for byte. The gate
sets `umask 022` internally because erofs-utils records the packed fragment
inode's source mode; this makes the fixture set independent of its caller.

## Real EROFS Fixture Oracle

The gate creates 35 deterministic EROFS images and seven external chunk blobs
in owned output. Nothing is committed as an image, blob, binary, or KLD. Each
of the five required storage paths has these seven cases:

- normal short target;
- normal 1,024-byte `MAXPATHLEN` target;
- zero-byte target;
- embedded NUL at the first, middle, and last byte;
- 1,025-byte over-limit target.

Inline positives are native mkfs symlinks. Plain, chunk, compact compressed
ztailpacking, and whole-file fragment cases start as real mkfs regular-file
carriers because the host cannot create a symlink containing NUL and mkfs does
not select those layouts for symlinks. The transform changes only inode mode,
the root dirent type, and, for empty cases, inode size, then recomputes the real
EROFS checksum. Chunk data remains on a real `--blobdev`; fragment data remains
in the real packed inode. Changed-byte offsets are recorded for every case.

The independent Python parser reads the superblock, checksum span, inode,
dirent, layout, and target bytes directly. It resolves chunk indexes and the
external blob, decodes compact inline-pcluster raw LZ4 without a DUT helper,
and resolves the whole-fragment offset through the packed inode. It does not
compile, call, or inject values into any DUT internal function. `dump.erofs`
is only a layout/type cross-check. All legal targets also pass
`fsck.erofs --extract` and reproduce the exact source bytes.

The independent decision model returns positive FreeBSD `EINTEGRITY` (97) for
empty or embedded-NUL targets, positive `ENAMETOOLONG` (63) before target I/O
for size greater than `MAXPATHLEN`, and success for exact non-NUL bytes. No
trailing NUL is required. All 35 expected results match, so every layout has a
bounded positive and negative oracle as required by G03.

## Linux and FreeBSD Semantics

Linux `erofs_fill_symlink()` validates only flat-inline fast symlinks while
building `i_link`; it uses `kmemdup_nul()`/`strlen()` and returns negative
`-EFSCORRUPTED`. Non-inline Linux symlinks use `page_get_link`. That page/cache
shape is not a FreeBSD implementation contract.

FreeBSD enters through `VOP_READLINK` on a vnode and currently streams through
`erofs_readlink_target`, the common map/read path, GEOM or the compressed
backing path, and `uiomove`. It has no Linux `i_link` or page-get-link path and
uses positive errno. B16 must therefore validate the complete immutable target
before any `uiomove`, use the existing layout-specific map/read machinery, and
keep the scan bounded by `MAXPATHLEN=1024`.

The 1,024-byte case is a successful `readlink(2)` byte target. Pathname follow
may still return `ENAMETOOLONG` when that target plus the remaining pathname
exceeds FreeBSD's namei buffer; that VFS result is distinct from the on-disk
target validator. The 1,025-byte inode is rejected by B16 before target I/O.

## Scope and Cleanup

The gate did not modify `src/**`, build a KLD, start QEMU, or run a feature
suite. It observed but did not signal or otherwise alter protected PID 26318,
port 9222, or `/work/debug-qemu/local/vm-freebsd-build.qcow2.bp`; their recorded
identity was unchanged before and after the run. Owned temporary source and
extract directories were removed, with zero leftovers.

B16 may now modify only its planned source/test write set and must pass the
specified host TC166, D, zstdio0 build, and targeted TC166 QEMU acceptance.
