Refuse a manifest entry whose mode is above 0777 on load (closes #225)
check / check (push) Canceled after 0s

Loading accepted an entry recording a mode above 0777, such as 04755.
check then reported a file with exactly the recorded permission bits
as a mode mismatch, list and export printed the out-of-range mode, and
freshen silently rewrote it as the permission bits alone. Loading now
refuses such an entry, naming the path and the mode, so every command
that reads a manifest refuses it with one message. fetch's own check
for this could no longer be reached and is gone; its test moves to the
loading tests in package mfer. docs/FORMAT.md says readers reject a
mode above 0777.

Model: opus-5-5
This commit was merged in pull request #230.
This commit is contained in:
2026-10-15 22:02:54 +02:00
parent 95a37d8009
commit 31e22a9337
5 changed files with 79 additions and 90 deletions
+61
View File
@@ -7,6 +7,7 @@ import (
"crypto/sha256"
"crypto/sha512"
"fmt"
"os"
"strconv"
"strings"
"testing"
@@ -234,6 +235,66 @@ func TestDeserializeRequiresExactlyOneSHA256Hash(t *testing.T) {
}
}
// A file entry that records a mode with more than the permission bits is
// refused as it is loaded, naming the path and the mode: once with setuid as
// Unix writes it (04755) and once as Go keeps it in os.FileMode. An entry
// recording 0777 loads.
func TestDeserializeRefusesModeOutsidePermissionBits(t *testing.T) {
t.Parallel()
hash, err := multihash.Encode(make([]byte, sha256.Size), multihash.SHA2_256)
require.NoError(t, err)
tests := []struct {
name string
mode uint32
want string // "" for an entry that loads
}{
{
"setuid as Unix writes it",
0o4755,
`file entry has a mode above 0777: "x" (04755)`,
},
{
"setuid as Go keeps it",
uint32(os.ModeSetuid | 0o755),
`file entry has a mode above 0777: "x" (040000755)`,
},
{"permission bits only", 0o777, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
id := uuid.NewV4()
inner, err := proto.Marshal(&MFFile{
Version: MFFile_VERSION_ONE,
Files: []*MFFilePath{{
Path: "x",
Size: 1,
Hashes: []*MFFileChecksum{{MultiHash: hash}},
Mode: tt.mode,
}},
Uuid: id[:],
})
require.NoError(t, err)
m, err := NewManifestFromReader(bytes.NewReader(wrapInner(t, id, inner)))
if tt.want == "" {
require.NoError(t, err)
require.Len(t, m.Files(), 1)
assert.Equal(t, tt.mode, m.Files()[0].GetMode())
return
}
require.ErrorIs(t, err, errModeOutOfRange)
require.EqualError(t, err, tt.want)
})
}
}
// Entries of a path, a SHA-256 hash, an empty MIME type and empty
// modification and change times are counted at 432 bytes each (176 + 112 +
// 16 + 64 + 64) and take 52 bytes plus the path to encode. A one-character