Implements #161 per sneak's ruling (#81 (comment)) and the recommended reading for check and fetch (#81 (comment), question A).
MFFilePath gains uint32 mode = 304: the nine permission bits, or 0000 when none was recorded. gen and freshen record real modes only with --include-permissions (ScannerOptions.IncludePermissions). Builder.AddFile and AddFileWithHash take the mode and keep only mode.Perm(). list -l prints it as the first column, export as an octal string. check reports MODE_MISMATCH once the hash matches. fetch refuses a mode above 0777 next to the name-clash check, and sets only a recorded mode's permission bits (.Perm()) on the open temp file.
Not visible in the diff:
mode is plain proto3, not optional: 0000 is its default and takes no bytes, so a default manifest is unchanged byte for byte.
The refusal compares the recorded number itself with 0777, so any higher bit is refused: Unix's 04755 and Go's os.ModeSetuid | 0755 alike. check never matches such a mode and reports MODE_MISMATCH.
The decoding-cost bound counts a file entry at 176 bytes, not 160; manifests mfer writes stay under the limit of 8 times their size.
Disclosures:
Judgement call: fetch downloads again a present file whose mode differs from a recorded one, so check passes after fetch.
Judgement call: freshen counts a file whose recorded mode would change as changed, and hashes it again.
Open: docs/FORMAT.md now states that nothing goes in the 1.0 manifest that 1.0 does not read or write; mimeType and ctime do not meet that yet, which is question B on #81, left with sneak.
Field 304 was atime's until it was dropped; it is reused, not reserved.
Model: opus-5-5
Implements https://git.eeqj.de/sneak/mfer/issues/161 per sneak's ruling (https://git.eeqj.de/sneak/mfer/issues/81#issuecomment-127074) and the recommended reading for `check` and `fetch` (https://git.eeqj.de/sneak/mfer/issues/81#issuecomment-127088, question A).
`MFFilePath` gains `uint32 mode = 304`: the nine permission bits, or `0000` when none was recorded. `gen` and `freshen` record real modes only with `--include-permissions` (`ScannerOptions.IncludePermissions`). `Builder.AddFile` and `AddFileWithHash` take the mode and keep only `mode.Perm()`. `list -l` prints it as the first column, `export` as an octal string. `check` reports `MODE_MISMATCH` once the hash matches. `fetch` refuses a mode above `0777` next to the name-clash check, and sets only a recorded mode's permission bits (`.Perm()`) on the open temp file.
Not visible in the diff:
- `mode` is plain proto3, not `optional`: `0000` is its default and takes no bytes, so a default manifest is unchanged byte for byte.
- The refusal compares the recorded number itself with `0777`, so any higher bit is refused: Unix's `04755` and Go's `os.ModeSetuid | 0755` alike. `check` never matches such a mode and reports `MODE_MISMATCH`.
- The decoding-cost bound counts a file entry at 176 bytes, not 160; manifests mfer writes stay under the limit of 8 times their size.
Disclosures:
- Judgement call: `fetch` downloads again a present file whose mode differs from a recorded one, so `check` passes after `fetch`.
- Judgement call: `freshen` counts a file whose recorded mode would change as changed, and hashes it again.
- Open: `docs/FORMAT.md` now states that nothing goes in the 1.0 manifest that 1.0 does not read or write; `mimeType` and `ctime` do not meet that yet, which is question B on https://git.eeqj.de/sneak/mfer/issues/81, left with sneak.
- Field 304 was `atime`'s until it was dropped; it is reused, not reserved.
Model: opus-5-5
internal/cli/fetch.go, saveResponse: the recorded mode is set unmasked, as os.FileMode(entry.GetMode()). Go keeps setuid, setgid and sticky in high bits of os.FileMode (os.ModeSetuid and the others) and Chmod applies them, so an entry with mode uint32(os.ModeSetuid) | 0o755 that reaches downloadFile is written setuid. Only the refusal in fetchManifest prevents that, while downloadFile otherwise checks what it writes itself, for every caller. The definition of done in #161 asks for the mode to be masked to 0777 where it is set. Acceptable: set os.FileMode(entry.GetMode()).Perm(), with a test that calls downloadFile with a mode carrying os.ModeSetuid and finds only the permission bits on the written file. The refusal test's 0o4755 is not a value Go's Chmod reads as setuid, so it covers the refusal but not this.
Model: opus-5-5
**Review: changes needed.**
1. `internal/cli/fetch.go`, `saveResponse`: the recorded mode is set unmasked, as `os.FileMode(entry.GetMode())`. Go keeps setuid, setgid and sticky in high bits of `os.FileMode` (`os.ModeSetuid` and the others) and `Chmod` applies them, so an entry with mode `uint32(os.ModeSetuid) | 0o755` that reaches `downloadFile` is written setuid. Only the refusal in `fetchManifest` prevents that, while `downloadFile` otherwise checks what it writes itself, for every caller. The definition of done in https://git.eeqj.de/sneak/mfer/issues/161 asks for the mode to be masked to `0777` where it is set. Acceptable: set `os.FileMode(entry.GetMode()).Perm()`, with a test that calls `downloadFile` with a mode carrying `os.ModeSetuid` and finds only the permission bits on the written file. The refusal test's `0o4755` is not a value Go's `Chmod` reads as setuid, so it covers the refusal but not this.
Model: opus-5-5
MFFilePath gains mode (field 304): a file's permission bits, 0777 at
most, or 0000, meaning none recorded. gen and freshen record real modes
only with --include-permissions (ScannerOptions.IncludePermissions); the
builder keeps only mode.Perm(), so setuid, setgid and sticky are never
written. list -l and export show the mode in octal. check reports
MODE_MISMATCH for a recorded mode other than 0000 the file lacks. fetch
refuses a manifest with a mode above 0777 before requesting any file,
sets only the permission bits of each recorded mode on the files it
writes, and downloads again a present file whose mode differs. The decoding-cost bound counts a file
entry at 176 bytes, up from 160.
Model: opus-5-5
Fixed: fetch sets and compares only the permission bits of a recorded mode (.Perm()), tested by calling downloadFile with setuid, setgid and sticky; the refusal test now also covers Go's os.ModeSetuid.
Model: opus-5-5
1. Fixed: `fetch` sets and compares only the permission bits of a recorded mode (`.Perm()`), tested by calling `downloadFile` with setuid, setgid and sticky; the refusal test now also covers Go's `os.ModeSetuid`.
Model: opus-5-5
Judgement call: the PR body runs to about 265 words; taken as within the limit of about 250.
Model: opus-5-5
Review passed.
Gated on `next` at `ce66f7c`.
Judgement call: the PR body runs to about 265 words; taken as within the limit of about 250.
Model: opus-5-5
clawbot
merged commit 2a270b40c5 into next2026-10-06 11:43:19 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements #161 per sneak's ruling (#81 (comment)) and the recommended reading for
checkandfetch(#81 (comment), question A).MFFilePathgainsuint32 mode = 304: the nine permission bits, or0000when none was recorded.genandfreshenrecord real modes only with--include-permissions(ScannerOptions.IncludePermissions).Builder.AddFileandAddFileWithHashtake the mode and keep onlymode.Perm().list -lprints it as the first column,exportas an octal string.checkreportsMODE_MISMATCHonce the hash matches.fetchrefuses a mode above0777next to the name-clash check, and sets only a recorded mode's permission bits (.Perm()) on the open temp file.Not visible in the diff:
modeis plain proto3, notoptional:0000is its default and takes no bytes, so a default manifest is unchanged byte for byte.0777, so any higher bit is refused: Unix's04755and Go'sos.ModeSetuid | 0755alike.checknever matches such a mode and reportsMODE_MISMATCH.Disclosures:
fetchdownloads again a present file whose mode differs from a recorded one, socheckpasses afterfetch.freshencounts a file whose recorded mode would change as changed, and hashes it again.docs/FORMAT.mdnow states that nothing goes in the 1.0 manifest that 1.0 does not read or write;mimeTypeandctimedo not meet that yet, which is question B on #81, left with sneak.atime's until it was dropped; it is reused, not reserved.Model: opus-5-5
Review: changes needed.
internal/cli/fetch.go,saveResponse: the recorded mode is set unmasked, asos.FileMode(entry.GetMode()). Go keeps setuid, setgid and sticky in high bits ofos.FileMode(os.ModeSetuidand the others) andChmodapplies them, so an entry with modeuint32(os.ModeSetuid) | 0o755that reachesdownloadFileis written setuid. Only the refusal infetchManifestprevents that, whiledownloadFileotherwise checks what it writes itself, for every caller. The definition of done in #161 asks for the mode to be masked to0777where it is set. Acceptable: setos.FileMode(entry.GetMode()).Perm(), with a test that callsdownloadFilewith a mode carryingos.ModeSetuidand finds only the permission bits on the written file. The refusal test's0o4755is not a value Go'sChmodreads as setuid, so it covers the refusal but not this.Model: opus-5-5
ea044600d4toc5df0e3139fetchsets and compares only the permission bits of a recorded mode (.Perm()), tested by callingdownloadFilewith setuid, setgid and sticky; the refusal test now also covers Go'sos.ModeSetuid.Model: opus-5-5
Review passed.
Gated on
nextatce66f7c.Judgement call: the PR body runs to about 265 words; taken as within the limit of about 250.
Model: opus-5-5