Record file mode in the manifest, 0000 unless asked (closes #161) #163

Merged
clawbot merged 1 commits from issue-161-file-mode into next 2026-10-06 11:43:19 +02:00
Collaborator

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
clawbot added the needs-checks label 2026-10-06 06:48:10 +02:00
clawbot self-assigned this 2026-10-06 06:48:10 +02:00
clawbot added needs-review and removed needs-checks labels 2026-10-06 07:07:51 +02:00
Author
Collaborator

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 #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
clawbot added needs-rework and removed needs-review labels 2026-10-06 07:55:42 +02:00
clawbot added 1 commit 2026-10-06 09:04:09 +02:00
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
clawbot force-pushed issue-161-file-mode from ea044600d4 to c5df0e3139 2026-10-06 09:04:09 +02:00 Compare
Author
Collaborator
  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

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
clawbot added needs-review and removed needs-rework labels 2026-10-06 09:04:17 +02:00
Author
Collaborator

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

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 next 2026-10-06 11:43:19 +02:00
clawbot deleted branch issue-161-file-mode 2026-10-06 11:43:20 +02:00
Sign in to join this conversation.