fix(copystructure): handle nil interfaces and reject unexported struct fields

copyValue panicked in two more places when given values that are not
plain YAML data:

- a struct field or pointer element holding a nil interface hit
  'reflect: call of reflect.Value.Set on zero Value', because
  reflect.ValueOf(nil) returns a zero Value that cannot be set.
- a struct with unexported fields, such as time.Time, hit
  'reflect.Value.Interface: cannot return value obtained from
  unexported field or method'.

Both are reachable from the SDK through CoalesceValues and
action.Install/Upgrade, and crash the calling process.

Nil interfaces are now stored as the zero value of the destination,
matching the slice fix in #31751. Structs with unexported fields now
return an "unsupported type" error, like the existing default case,
since Helm only needs to deep copy POD values.

Closes #32711

Signed-off-by: Nitin Kumar <nitinraj844126@gmail.com>
pull/32712/head
Nitin Kumar 20 hours ago
parent 53dfa521e0
commit 3a1d6b4db0
No known key found for this signature in database

@ -80,7 +80,7 @@ func copyValue(original reflect.Value) (any, error) {
return nil, err return nil, err
} }
ptr := reflect.New(original.Type().Elem()) ptr := reflect.New(original.Type().Elem())
ptr.Elem().Set(reflect.ValueOf(copied)) setValue(ptr.Elem(), copied)
return ptr.Interface(), nil return ptr.Interface(), nil
case reflect.Slice: case reflect.Slice:
@ -106,13 +106,20 @@ func copyValue(original reflect.Value) (any, error) {
return copied.Interface(), nil return copied.Interface(), nil
case reflect.Struct: case reflect.Struct:
copied := reflect.New(original.Type()).Elem() t := original.Type()
for field := range t.Fields() {
// Unexported fields can't be read or set through reflection.
if !field.IsExported() {
return nil, fmt.Errorf("unsupported type %v: struct with unexported fields", t)
}
}
copied := reflect.New(t).Elem()
for i := 0; i < original.NumField(); i++ { for i := 0; i < original.NumField(); i++ {
elem, err := copyValue(original.Field(i)) elem, err := copyValue(original.Field(i))
if err != nil { if err != nil {
return nil, err return nil, err
} }
copied.Field(i).Set(reflect.ValueOf(elem)) setValue(copied.Field(i), elem)
} }
return copied.Interface(), nil return copied.Interface(), nil
@ -126,3 +133,13 @@ func copyValue(original reflect.Value) (any, error) {
return original.Interface(), fmt.Errorf("unsupported type %v", original) return original.Interface(), fmt.Errorf("unsupported type %v", original)
} }
} }
// setValue sets dst to v. A nil v (the copy of a nil interface) is stored as
// the zero value of dst's type, since reflect.ValueOf(nil) can't be passed to Set.
func setValue(dst reflect.Value, v any) {
if v == nil {
dst.Set(reflect.Zero(dst.Type()))
return
}
dst.Set(reflect.ValueOf(v))
}

@ -18,6 +18,7 @@ package copystructure
import ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -193,37 +194,67 @@ func TestCopy_Map(t *testing.T) {
} }
func TestCopy_Struct(t *testing.T) { func TestCopy_Struct(t *testing.T) {
type TestStruct struct { t.Run("exported fields", func(t *testing.T) {
Name string type TestStruct struct {
Age int Name string
Active bool Age int
Scores []int Active bool
Metadata map[string]any Scores []int
} Metadata map[string]any
}
input := TestStruct{ input := TestStruct{
Name: "John", Name: "John",
Age: 30, Age: 30,
Active: true, Active: true,
Scores: []int{95, 87, 92}, Scores: []int{95, 87, 92},
Metadata: map[string]any{ Metadata: map[string]any{
"level": "advanced", "level": "advanced",
"tags": []string{"go", "programming"}, "tags": []string{"go", "programming"},
}, },
} }
result, err := Copy(input) result, err := Copy(input)
require.NoError(t, err) require.NoError(t, err)
resultStruct, ok := result.(TestStruct) resultStruct, ok := result.(TestStruct)
require.True(t, ok) require.True(t, ok)
assert.Equal(t, input, resultStruct) assert.Equal(t, input, resultStruct)
// Verify deep copy
input.Name = "Modified"
input.Scores[0] = 999
assert.Equal(t, "John", resultStruct.Name)
assert.Equal(t, 95, resultStruct.Scores[0])
})
t.Run("nil interface field", func(t *testing.T) {
type WithAny struct {
Name string
Extra any
}
// Verify deep copy input := map[string]any{"s": WithAny{Name: "a"}}
input.Name = "Modified"
input.Scores[0] = 999 result, err := Copy(input)
assert.Equal(t, "John", resultStruct.Name) require.NoError(t, err)
assert.Equal(t, 95, resultStruct.Scores[0]) assert.Equal(t, input, result)
})
t.Run("unexported fields", func(t *testing.T) {
type withUnexported struct {
Name string
secret int
}
_, err := Copy(map[string]any{"s": withUnexported{Name: "a", secret: 1}})
require.Error(t, err)
})
t.Run("time.Time", func(t *testing.T) {
_, err := Copy(map[string]any{"t": time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)})
require.Error(t, err)
})
} }
func TestCopy_Pointer(t *testing.T) { func TestCopy_Pointer(t *testing.T) {
@ -269,6 +300,18 @@ func TestCopy_Pointer(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Nil(t, result) assert.Nil(t, result)
}) })
t.Run("pointer to nil interface", func(t *testing.T) {
var value any
result, err := Copy(&value)
require.NoError(t, err)
resultPtr, ok := result.(*any)
require.True(t, ok)
assert.Nil(t, *resultPtr)
assert.NotSame(t, &value, resultPtr)
})
} }
func TestCopy_Interface(t *testing.T) { func TestCopy_Interface(t *testing.T) {

@ -329,6 +329,13 @@ func TestInstallRelease_NoName(t *testing.T) {
assert.ErrorContains(t, err, "no name provided") assert.ErrorContains(t, err, "no name provided")
} }
func TestInstallRelease_UnsupportedValueType(t *testing.T) {
instAction := installAction(t)
vals := map[string]any{"deployedAt": time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)}
_, err := instAction.Run(buildChart(), vals)
assert.ErrorContains(t, err, "struct with unexported fields")
}
func TestInstallRelease_WithNotes(t *testing.T) { func TestInstallRelease_WithNotes(t *testing.T) {
is := assert.New(t) is := assert.New(t)
req := require.New(t) req := require.New(t)

Loading…
Cancel
Save