@cryptotaxi247 / kubo / commits / 00ec99ab6

fix: validate --max-hamt-fanout CLI flag per UnixFS spec (#11230)

* fix: validate --max-hamt-fanout CLI flag per UnixFS spec the CLI flag bypassed the config validation in ValidateImportConfig, allowing spec-noncompliant values (e.g. 3 or 999999) to be silently accepted. validation now happens in the options layer, covering both CLI and programmatic API usage. * fix: simplify HAMT fanout constraint phrasing Co-authored-by: Guillaume Michel <15075495+guillaumemichel@users.noreply.github.com>

Marcin Rataj committed Mar 15, 2026 at 23:52 UTC 00ec99ab68be95b9e2c8ac0967896ccaea623c11
6 files changed +45 -19
config/import.go
+2 -3
@@ -102,10 +102,9 @@ func ValidateImportConfig(cfg *Import) error {
102 if !cfg.UnixFSHAMTDirectoryMaxFanout.IsDefault() {
103 fanout := cfg.UnixFSHAMTDirectoryMaxFanout.WithDefault(DefaultUnixFSHAMTDirectoryMaxFanout)
104
105 - // Check all requirements: fanout < 8 covers both non-positive and non-multiple of 8
106 - // Combined with power of 2 check and max limit, this ensures valid values: 8, 16, 32, 64, 128, 256, 512, 1024
105 + // Valid values are powers of 2 between 8 and 1024: 8, 16, 32, 64, 128, 256, 512, 1024
106 if fanout < 8 || !isPowerOfTwo(fanout) || fanout > 1024 {
108 - return fmt.Errorf("Import.UnixFSHAMTDirectoryMaxFanout must be a positive power of 2, multiple of 8, and not exceed 1024 (got %d)", fanout)
107 + return fmt.Errorf("Import.UnixFSHAMTDirectoryMaxFanout must be a power of 2, between 8 and 1024 (got %d)", fanout)
108 }
109 }
110
config/import_test.go
+14 -14
@@ -26,25 +26,25 @@ func TestValidateImportConfig_HAMTFanout(t *testing.T) {
26 {name: "valid 1024", fanout: 1024, wantErr: false},
27
28 // Invalid values - not powers of 2
29 - {name: "invalid 7", fanout: 7, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
30 - {name: "invalid 15", fanout: 15, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
31 - {name: "invalid 100", fanout: 100, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
32 - {name: "invalid 257", fanout: 257, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
33 - {name: "invalid 1000", fanout: 1000, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
29 + {name: "invalid 7", fanout: 7, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
30 + {name: "invalid 15", fanout: 15, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
31 + {name: "invalid 100", fanout: 100, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
32 + {name: "invalid 257", fanout: 257, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
33 + {name: "invalid 1000", fanout: 1000, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
34
35 - // Invalid values - powers of 2 but not multiples of 8
36 - {name: "invalid 1", fanout: 1, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
37 - {name: "invalid 2", fanout: 2, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
38 - {name: "invalid 4", fanout: 4, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
35 + // Invalid values - powers of 2 but less than 8
36 + {name: "invalid 1", fanout: 1, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
37 + {name: "invalid 2", fanout: 2, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
38 + {name: "invalid 4", fanout: 4, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
39
40 // Invalid values - exceeds 1024
41 - {name: "invalid 2048", fanout: 2048, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
42 - {name: "invalid 4096", fanout: 4096, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
41 + {name: "invalid 2048", fanout: 2048, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
42 + {name: "invalid 4096", fanout: 4096, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
43
44 // Invalid values - negative or zero
45 - {name: "invalid 0", fanout: 0, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
46 - {name: "invalid -8", fanout: -8, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
47 - {name: "invalid -256", fanout: -256, wantErr: true, errMsg: "must be a positive power of 2, multiple of 8, and not exceed 1024"},
45 + {name: "invalid 0", fanout: 0, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
46 + {name: "invalid -8", fanout: -8, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
47 + {name: "invalid -256", fanout: -256, wantErr: true, errMsg: "must be a power of 2, between 8 and 1024"},
48 }
49
50 for _, tt := range tests {
core/commands/add.go
+1 -1
@@ -253,7 +253,7 @@ https://github.com/ipfs/kubo/blob/master/docs/config.md#import
253 // Advanced UnixFS Limits
254 cmds.IntOption(maxFileLinksOptionName, "Limit the maximum number of links in UnixFS file nodes to this value. WARNING: experimental. Default: Import.UnixFSFileMaxLinks"),
255 cmds.IntOption(maxDirectoryLinksOptionName, "Limit the maximum number of links in UnixFS basic directory nodes to this value. WARNING: experimental, Import.UnixFSHAMTDirectorySizeThreshold is safer. Default: Import.UnixFSDirectoryMaxLinks"),
256 - cmds.IntOption(maxHAMTFanoutOptionName, "Limit the maximum number of links of a UnixFS HAMT directory node to this (power of 2, multiple of 8). WARNING: experimental, Import.UnixFSHAMTDirectorySizeThreshold is safer. Default: Import.UnixFSHAMTDirectoryMaxFanout"),
256 + cmds.IntOption(maxHAMTFanoutOptionName, "Limit the maximum number of links of a UnixFS HAMT directory node to this (power of 2, between 8 and 1024). WARNING: experimental, Import.UnixFSHAMTDirectorySizeThreshold is safer. Default: Import.UnixFSHAMTDirectoryMaxFanout"),
257 // Experimental Features
258 cmds.BoolOption(inlineOptionName, "Inline small blocks into CIDs. WARNING: experimental"),
259 cmds.IntOption(inlineLimitOptionName, fmt.Sprintf("Maximum block size to inline. Maximum: %d bytes. WARNING: experimental", verifcid.DefaultMaxIdentityDigestSize)).WithDefault(32),
core/coreiface/options/unixfs.go
+5
@@ -233,8 +233,13 @@ func (unixfsOpts) MaxDirectoryLinks(n int) UnixfsAddOption {
233 }
234
235 // MaxHAMTFanout specifies the maximum width of the HAMT directory shards.
236 +// Per the UnixFS spec, the value must be a power of 2, minimum 8
237 +// (for byte-aligned bitfields), and maximum 1024.
238 func (unixfsOpts) MaxHAMTFanout(n int) UnixfsAddOption {
239 return func(settings *UnixfsAddSettings) error {
240 + if n < 8 || n&(n-1) != 0 || n > 1024 {
241 + return fmt.Errorf("HAMT fanout must be a power of 2, between 8 and 1024 (got %d)", n)
242 + }
243 settings.MaxHAMTFanout = n
244 settings.MaxHAMTFanoutSet = true
245 return nil
core/coreiface/options/unixfs_test.go new
+22
@@ -0,0 +1,22 @@
1 +package options
2 +
3 +import (
4 + "testing"
5 +
6 + "github.com/stretchr/testify/require"
7 +)
8 +
9 +func TestMaxHAMTFanoutValidation(t *testing.T) {
10 + valid := []int{8, 16, 32, 64, 128, 256, 512, 1024}
11 + for _, v := range valid {
12 + _, _, err := UnixfsAddOptions(Unixfs.MaxHAMTFanout(v))
13 + require.NoError(t, err, "fanout %d should be valid", v)
14 + }
15 +
16 + invalid := []int{-1, 0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 12, 24, 48, 100, 2048, 4096, 999999}
17 + for _, v := range invalid {
18 + _, _, err := UnixfsAddOptions(Unixfs.MaxHAMTFanout(v))
19 + require.Error(t, err, "fanout %d should be invalid", v)
20 + require.Contains(t, err.Error(), "HAMT fanout must be")
21 + }
22 +}
docs/config.md
+1 -1
@@ -3858,7 +3858,7 @@ become too big or reach `MaxLinks`. A HAMT is a structure made of UnixFS
3858 nodes that store the list of elements in the folder. This option controls the
3859 maximum number of children that the HAMT nodes can have.
3860
3861 -According to the [UnixFS specification](https://specs.ipfs.tech/unixfs/#hamt-structure-and-parameters), this value must be a power of 2, a multiple of 8 (for byte-aligned bitfields), and not exceed 1024 (to prevent denial-of-service attacks).
3861 +According to the [UnixFS specification](https://specs.ipfs.tech/unixfs/#hamt-structure-and-parameters), this value must be a power of 2, between 8 (for byte-aligned bitfields) and 1024 (to prevent denial-of-service attacks).
3862
3863 Commands affected: `ipfs add`, `ipfs daemon` (globally overrides [`boxo/ipld/unixfs/io.DefaultShardWidth`](https://github.com/ipfs/boxo/blob/6c5a07602aed248acc86598f30ab61923a54a83e/ipld/unixfs/io/directory.go#L30C5-L30C22))
3864