overlay: don't override a configured index mount option

NewSnapshotter auto-appends "index=off" unless the configured
mount_options already carry an index option — but the check used
hasOption(..., "index", false), which compares against the literal
string "index". No valid mount option ever equals "index" (it is
always valued, "index=on"/"index=off"), so the append happened
unconditionally whenever the kernel exposes the overlay index
parameter.

Mount options are last-wins in the kernel, so a user-configured
"index=on" was silently overridden. Worse, configuring
mount_options = ["index=on", "nfs_export=on"] (the documented way to
make overlay mounts NFS-exportable) produced
"index=on,nfs_export=on,index=off", which the kernel rejects with
EINVAL (nfs_export=on conflicts with an explicit index=off) — breaking
every snapshot mount including image unpack.

Make hasOption match the key of a "key[=value]" option so callers need
not care whether the option takes a value, and add a regression test.

Signed-off-by: AprilNEA <github@sku.moe>
This commit is contained in:
AprilNEA
2026-07-17 13:37:54 +08:00
parent 94aa7cdd48
commit 5e25f36e5e
2 changed files with 34 additions and 8 deletions

View File

@@ -147,7 +147,7 @@ func NewSnapshotter(root string, opts ...Opt) (snapshots.Snapshotter, error) {
return nil, err
}
if !hasOption(config.mountOptions, "userxattr", false) {
if !hasOption(config.mountOptions, "userxattr") {
// figure out whether "userxattr" option is recognized by the kernel && needed
userxattr, err := overlayutils.NeedsUserXAttr(root)
if err != nil {
@@ -158,7 +158,9 @@ func NewSnapshotter(root string, opts ...Opt) (snapshots.Snapshotter, error) {
}
}
if !hasOption(config.mountOptions, "index", false) && supportsIndex() {
// Mount options are last-wins in the kernel, so appending "index=off"
// after a configured "index=on" would silently override it.
if !hasOption(config.mountOptions, "index") && supportsIndex() {
config.mountOptions = append(config.mountOptions, "index=off")
}
@@ -173,13 +175,14 @@ func NewSnapshotter(root string, opts ...Opt) (snapshots.Snapshotter, error) {
}, nil
}
func hasOption(options []string, key string, hasValue bool) bool {
// hasOption reports whether the "key" option is present in options, either
// as a bare flag ("userxattr") or in "key=value" form ("index=on").
func hasOption(options []string, key string) bool {
for _, option := range options {
if hasValue {
if strings.HasPrefix(option, key) && len(option) > len(key) && option[len(key)] == '=' {
return true
}
} else if option == key {
if option == key {
return true
}
if optionKey, _, ok := strings.Cut(option, "="); ok && optionKey == key {
return true
}
}

View File

@@ -48,6 +48,29 @@ func newSnapshotterWithOpts(opts ...Opt) testsuite.SnapshotterFunc {
}
}
// NewSnapshotter must not auto-append "index=off" when the configuration
// already carries an index option: mount options are last-wins in the
// kernel, so the appended "index=off" would silently override a configured
// "index=on" — and combined with "nfs_export=on" it makes every overlay
// mount fail with EINVAL (nfs_export=on conflicts with an explicit
// index=off).
func TestOverlayConfiguredIndexNotOverridden(t *testing.T) {
if !supportsIndex() {
t.Skip("kernel does not expose the overlay index parameter")
}
sn, err := NewSnapshotter(t.TempDir(), WithMountOptions([]string{"index=on", "nfs_export=on"}))
if err != nil {
t.Fatal(err)
}
defer sn.Close()
options := sn.(*snapshotter).options
for _, opt := range options {
if opt == "index=off" {
t.Fatalf("configured index option overridden by auto-appended index=off (options: %v)", options)
}
}
}
func TestOverlay(t *testing.T) {
testutil.RequiresRoot(t)
optTestCases := map[string][]Opt{