# Functional Correctness Audit Reviewed revision: `47944d22d1ad45a7f537780effb3d05f998eef14` (Update ReleaseKit to v0.4.2). Audit date: 2026-10-25. ## Scope And Evidence This audit reviewed the root facade and its engine handoffs, source/discovery and filesystem handling, composition and collection rules, inheritance, typed decoding and ownership, application callbacks, secrets, metadata, explanation, and effective-value comparison against the configuration contract and relevant implementation/tests. It was a source-level functional review with targeted diagnostics, not an exhaustive proof or a performance/security certification. The existing development check (`./scripts/check.sh`) and full race-enabled suite (`go test -race ./...`) passed using Go 1.26.10 on Linux amd64. Standalone public-API probes outside the repository reproduced the findings below. No production code or repository tests were added or changed. No native macOS or Windows validation was performed, and no new release gates are proposed. ## 1. Pointer-Only Recursive Types Can Overflow The Stack Or Hang Status: open. Priority: medium. Category: schema/decoder termination. **Relevant code.** `schemaBuilder.describe` in [schema.go](../../internal/engine/schema.go), lines 119–140, caches a placeholder before describing pointer elements. A legal Go type such as `type Loop *Loop` therefore produces a pointer descriptor whose element is itself. `decodeValue` in [decode.go](../../internal/engine/decode.go), lines 93–106, recursively unwraps pointers without consuming the input node. The pointer unwrapping loops in `resolveSchemaPath` (`schema.go`, lines 347–350 and 387–390) also assume that a non-pointer descriptor will eventually be reached. **Reproduction.** With the public package imported as `configkit`: ```go type Loop *Loop type Config struct { Value Loop `yaml:"value"` } configkit.Load[Config](configkit.Bytes("input", []byte("value: {}\n"))) ``` This does not return a decode error. It repeatedly calls `decodeValue` with the same input and descriptor until the process reports a fatal stack overflow. A disposable probe using `debug.SetMaxStack(64 << 10)` demonstrated the failure without allowing the process to grow a large stack. Explicit `value: null` loads successfully. With that null input, adding a resolver that calls `Normalize` on `/value` with `Loop(nil)` instead hangs in `resolveSchemaPath`; the separate diagnostic process was stopped by a three-second timeout. **Expected behavior.** Incompatible non-null input must fail with a safe diagnostic, and schema-path resolution must terminate. The [schema contract](../config.md#application-schema-binding) supports recursive types with acyclic values; the existing explicit pointer-only restriction is for inline fields, not all pointer fields. This is not a request to recover application panics or reject ordinary recursive structs. **Potential remediation.** Detect repeated descriptors while unwrapping pointer-only chains in decoding and schema-path traversal. Preserve supported nil/null values and finite acyclic values, return an appropriate decode error for an incompatible non-null node, and reject descendants that cannot be addressed. Review analogous pointer-unwrapping helpers for the same assumption. A blanket schema rejection of pointer-only recursive types would instead be a contract decision, since it would also reject currently valid nil values; do not introduce it or an arbitrary pointer-depth cap merely to stop the loop. ## 2. Failed Dynamic Schema Construction Leaves A Reusable Partial Descriptor Status: open. Priority: medium. Category: error handling and shared operation state. **Relevant code.** `valueOperation.dynamicDescriptor` in [value_operation.go](../../internal/engine/value_operation.go), lines 36–48, passes the load's existing descriptor cache directly to a new schema builder. `schemaBuilder.describe` in [schema.go](../../internal/engine/schema.go), lines 119–140 and 167–191, caches container placeholders but returns child errors without removing those placeholders. A subsequent lookup returns the partial descriptor as if construction had succeeded. **Reproduction.** Use a configuration with `Value any` and this dynamic type: ```go type Invalid struct { Channel chan int `yaml:"channel"` } ``` In one resolver, execute these two assignments in order: ```go configkit.Normalize(r, configkit.Keys("value"), &c.Value, any((*Invalid)(nil)), "first unsupported value") configkit.Normalize(r, configkit.Keys("value"), &c.Value, any(&Invalid{}), "second unsupported value") ``` The first helper correctly records an invalid-value diagnostic because channels are unsupported. It nevertheless leaves the `*Invalid` descriptor cached with a nil element. The second helper reuses it, reaches projection with that nil element, and panics with a nil-pointer dereference instead of returning a `LoadError`. This was reproduced through ordinary public `Load` and `Normalize`; the application callback itself does not panic. **Expected behavior.** Unsupported dynamic/helper values must produce safe schema/resolution diagnostics under the [typed projection](../config.md#typed-projection-and-ownership) and [resolution contracts](../config.md#recorded-application-resolution). One reported validation failure must not corrupt the descriptor cache used by later operations in the same callback. **Potential remediation.** Make dynamic descriptor construction transactional: stage newly discovered descriptors and commit them only after a successful build, or track and roll back all cache additions belonging to a failed build. Account for recursive descriptors that refer to other partially built types, not just the immediate failed container. Preserve previously valid descriptors and the load-local cache ownership. A nil guard in projection alone would not repair invalid cache entries being treated as successful schema descriptions. ## 3. Cycle Detection Rejects Acyclic Slice Views Sharing A Starting Address Status: open. Priority: medium. Category: supported-value correctness. **Relevant code.** `valueVisit` and `visitFor` in [value_operation.go](../../internal/engine/value_operation.go), lines 20–24 and 97–116, identify a slice by its type, kind, and starting pointer, but not its length. Both reflective projection/cloning and the parser-neutral value builder in [value.go](../../internal/engine/value.go) use that identity for active-path cycle detection. **Reproduction.** This value is finite and acyclic: its nested one-element view contains only the string, not the enclosing two-element view. ```go shared := []any{"leaf", nil} shared[1] = shared[:1] ``` Place it at `data` in a `Mapping` source, return it from typed defaults for `Data []any`, or supply it as a CLI override. Observed results at `/data/1` were, respectively, `source.invalid`, `schema.cycle`, and `override.invalid_value`. The equivalent independently allocated value `[]any{"leaf", []any{"leaf"}}` loads and renders successfully. A true self-referential slice was also checked and remains correctly rejected. **Expected behavior.** The [mapping source](../config.md#sources), [projection/ownership](../config.md#typed-projection-and-ownership), and [override](../config.md#explicit-cli-overrides) contracts permit supported acyclic values. Shared backing storage alone is not a cycle, and detaching it does not require preserving alias identity in the returned configuration. **Potential remediation.** Include the visible slice extent (its length) in the active-visit identity, or use an equivalent identity that distinguishes these views. Preserve detection of actual self/mutual cycles, empty/nil distinctions, and independent ownership of retained values. Cover the shared helper's source, override, projection, and cloning callers rather than fixing only one entry point. ## 4. Callback Contexts Remain Active After Panic Unwinding Status: open. Priority: low. Category: callback-lifetime contract. **Relevant code.** `runResolution` in [resolution.go](../../internal/engine/resolution.go), lines 283–291, and `runValidation`/`reportVersion` in [validation.go](../../internal/engine/validation.go), lines 99–106 and 250–257, set `state.active = false` only after the application callback returns normally. The root facade forwards the context without an additional lifetime guard. **Reproduction.** Retain the supplied context in a resolver, semantic validator, or version checker, then panic in that callback. Recover the application panic outside `Load` and invoke the retained context. Public-API probes showed that `Resolution.IsExplicit` still returns a value and `Validation.Warning` still accepts a report for both validation and version contexts; none produces the documented closed-context panic. **Expected behavior.** The [resolution](../config.md#recorded-application-resolution) and [validation](../config.md#semantic-validation) contracts make these contexts callback-scoped and require retained contexts to reject use after callback exit. Application panics remain programming failures and should propagate unchanged; closing the context during unwinding does not require ConfigKit to recover them or return a partial result. This finding concerns only the lifetime guard after an application chooses to recover its own panic. **Potential remediation.** Deactivate the context with a defer scoped to each callback invocation, retaining immediate deactivation on normal return as well as on panic unwinding. Do not leave the context active through later projection or metadata work, and do not convert or suppress the callback panic. Apply the same lifetime handling to resolution, semantic validation, and version checks.