Skip to content

configutil: don't panic on a struct-slice field with the wrong YAML shape - #4282

Merged
roman-khimov merged 4 commits into
nspcc-dev:masterfrom
fabwa:fix/config-struct-slice-type-assertion
Nov 10, 2026
Merged

roman-khimov merged 4 commits into
nspcc-dev:masterfrom
fabwa:fix/config-struct-slice-type-assertion

Conversation

@fabwa

@fabwa fabwa commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Summary

processStructSlice asserted the raw viper value for a struct-slice field directly to []any, and each element directly to map[string]any, both without checking the assertion succeeded. A config field declared as a slice of structs (e.g. Config.GRPC, a shard list) given the wrong shape in YAML - a scalar, or a single mapping instead of a list of mappings - panics with interface conversion: interface {} is ..., not []any instead of being rejected with a clean error.

This is reachable not just at startup (cmd/neofs-node/main.go) but via SIGHUP config reload on an already-running node (cmd/neofs-node/config.go's configWatcher/reloadConfig, neither of which recovers from a panic), turning an operator's config typo into an unplanned process crash instead of a rejected reload.

Fix

processPrimitiveSlice, right below in the same file, already handles the identical situation safely with the comma-ok form: existing, _ := raw.([]any). This PR matches that existing pattern instead of introducing a new error-handling convention: use comma-ok for both assertions, falling back to the already-present zero values (nil list, empty item map) when the stored value isn't shaped as expected.

This doesn't silently swallow the malformed config - viper's value is left untouched, so UnmarshalExact's own mapstructure-based decoding (which already returns a clean, descriptive error for a type mismatch against the destination struct) is reached instead of being pre-empted by a panic.

Test plan

  • Added TestEnvUnmarshal_StructSliceAsScalar, confirming Unmarshal returns a clean error instead of panicking when a struct-slice field is given a scalar value.
  • Confirmed this test panics against the unpatched code and passes after the fix.
  • go vet ./... clean; go test ./internal/configutil/... ./cmd/neofs-node/... passes.
  • Patch verified to apply cleanly and pass tests in a separate clean worktree against the current master tip.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 32.71%. Comparing base (34d007f) to head (7136a21).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #4282   +/-   ##
=======================================
  Coverage   32.70%   32.71%           
=======================================
  Files         673      673           
  Lines       41698    41701    +3     
=======================================
+ Hits        13638    13643    +5     
+ Misses      28060    28058    -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread internal/configutil/env.go Resolved
Comment thread CHANGELOG.md Resolved
Comment thread internal/configutil/env.go Resolved
Comment thread internal/configutil/env.go Resolved
@fabwa
fabwa force-pushed the fix/config-struct-slice-type-assertion branch from 4aceaac to 09dcf73 Compare November 10, 2026 16:16

@roman-khimov roman-khimov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next one to get in. Please fix conflicts.

Fabian Wahle and others added 4 commits November 10, 2026 20:25
…hape

processStructSlice asserted the raw viper value for a struct-slice
field directly to []any, and each element directly to map[string]any,
both without checking the assertion succeeded. A config field declared
as a slice of structs (e.g. Config.GRPC, a shard list) given the wrong
shape in YAML - a scalar, or a single mapping instead of a list of
mappings - panics with "interface conversion: interface {} is ...,
not []any" instead of being rejected with a clean error.

This is reachable not just at startup (cmd/neofs-node/main.go) but via
SIGHUP config reload on an already-running node
(cmd/neofs-node/config.go's configWatcher/reloadConfig, neither of
which recovers from a panic), turning an operator's config typo into
an unplanned process crash instead of a rejected reload.

processPrimitiveSlice, right below in the same file, already handles
the identical situation safely with the comma-ok form:
`existing, _ := raw.([]any)`. Match that existing pattern instead of
introducing a new error-handling convention: use comma-ok for both
assertions, falling back to the already-present zero values (nil list,
empty item map) when the stored value isn't shaped as expected. This
doesn't silently swallow the malformed config - viper's value is left
untouched, so UnmarshalExact's own mapstructure-based decoding (which
already returns a clean, descriptive error for a type mismatch against
the destination struct) is reached instead of being pre-empted by a
panic.

Signed-off-by: Fabian Wahle <fabian@nash.io>
Review feedback: tolerating a malformed list item by substituting an
empty map meant that an env override on any item of the same list made
processStructSlice rebuild it, and the caller v.Set it back over the
original - so UnmarshalExact never saw the bad value. The config then
loaded with no error at all and a zero-valued entry in its place,
trading the original panic for a silent misconfiguration. The scalar
case had the same hole; its existing test missed it by not setting an
override.

Check the shape up front instead and leave the value alone when it is
wrong, so mapstructure's own type check reports it:

    'clients[1]' expected a map or struct, got "string"

Returning nil is already this function's way of saying it has nothing
to override, so no new error plumbing is needed through its call
sites. The pre-scan runs before the loop because item maps are
references into viper's parsed config: bailing out mid-loop would
leave earlier items already mutated in place.

Regression tests cover all four combinations (scalar/malformed item,
with and without an env override); the two override ones fail against
the previous commit.

Signed-off-by: Fabian Wahle <fabian@nash.io>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review feedback.

Signed-off-by: Fabian Wahle <fabian@nash.io>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The shape of every item is checked before the loop, so the branch was dead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Fabian Wahle <fabian@nash.io>
@fabwa
fabwa force-pushed the fix/config-struct-slice-type-assertion branch from 09dcf73 to 7136a21 Compare November 10, 2026 19:34
@roman-khimov
roman-khimov merged commit 1193c44 into nspcc-dev:master Nov 10, 2026
19 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants