Skip to content

Commit e96c40c

Browse files
tas50claude
andauthored
šŸ› llx: stop array contains/difference builtins panicking on a null argument (#8320)
* šŸ› llx: stop array contains/difference builtins panicking on a null argument `arrayContainsAll`, `arrayContainsNone`, and `arrayDifferenceV2` guarded the receiver array against null but then did an unchecked `arg.Value.([]any)` on the argument. When the argument resolves to a typed null array — e.g. a `map[string][]T` key miss such as `pam.conf.services["su"]` on a host with no `/etc/pam.d` (COS, Flatcar, Bottlerocket k8s nodes) — that assertion panics with `interface conversion: interface {} is nil, not []interface {}`. Because the executor runs blocks in goroutines the panic is unrecoverable and crashes the entire scan rather than failing the single check. The dict variants already use the safe comma-ok form; only the array variants were affected. Guard the argument the same way the receiver is guarded: a null argument propagates as null. The compiled `… == []` wrapper then resolves the check to a clean pass/fail instead of crashing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 🟢 core: lock containsAll-of-empty-list returns true mondoo-linux-security's su-restriction check relies on `groups.containsAll(suRestrictedGroups)` being true when suRestrictedGroups is an empty (typed, non-null) []string. Add a regression test pinning that containsAll of an empty list is vacuously satisfied, distinct from the null-arg case which propagates null. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 379da11 commit e96c40c

2 files changed

Lines changed: 49 additions & 0 deletions

File tree

ā€Žllx/builtin_array.goā€Ž

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,14 @@ func arrayDifferenceV2(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64
815815
return nil, rref, err
816816
}
817817

818+
// A null argument (e.g. a missing map key resolving to a typed null
819+
// array) propagates as null, mirroring the receiver-nil guard above.
820+
// Without this the `arg.Value.([]any)` assertion below panics, which
821+
// crashes the whole scan rather than failing the single check.
822+
if arg.Value == nil {
823+
return &RawData{Type: bind.Type, Error: arg.Error}, 0, nil
824+
}
825+
818826
t := types.Type(arg.Type)
819827
if t != bind.Type {
820828
return nil, 0, errors.New("called `difference` with wrong type (got: " + t.Label() + ", expected:" + bind.Type.Label() + ")")
@@ -920,6 +928,14 @@ func arrayContainsAll(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64)
920928
return nil, rref, err
921929
}
922930

931+
// A null argument (e.g. a missing map key resolving to a typed null
932+
// array) propagates as null, mirroring the receiver-nil guard above.
933+
// Without this the `arg.Value.([]any)` assertion below panics, which
934+
// crashes the whole scan rather than failing the single check.
935+
if arg.Value == nil {
936+
return &RawData{Type: bind.Type, Error: arg.Error}, 0, nil
937+
}
938+
923939
t := types.Type(arg.Type)
924940
if t != bind.Type {
925941
return nil, 0, errors.New("called `arrayNone` with wrong type (got: " + t.Label() + ", expected:" + bind.Type.Label() + ")")
@@ -973,6 +989,14 @@ func arrayContainsNone(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64
973989
return nil, rref, err
974990
}
975991

992+
// A null argument (e.g. a missing map key resolving to a typed null
993+
// array) propagates as null, mirroring the receiver-nil guard above.
994+
// Without this the `arg.Value.([]any)` assertion below panics, which
995+
// crashes the whole scan rather than failing the single check.
996+
if arg.Value == nil {
997+
return &RawData{Type: bind.Type, Error: arg.Error}, 0, nil
998+
}
999+
9761000
t := types.Type(arg.Type)
9771001
if t != bind.Type {
9781002
return nil, 0, errors.New("called `arrayNone` with wrong type (got: " + t.Label() + ", expected:" + bind.Type.Label() + ")")

ā€Žproviders/core/resources/mql_test.goā€Ž

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,31 @@ func TestArray(t *testing.T) {
741741
Code: "['x'].containsAll([asset.labels['nope']])",
742742
Expectation: []any{nil},
743743
},
744+
// Regression: a null argument (e.g. a missing map key resolving to a
745+
// typed null array, as happens on hosts with no /etc/pam.d) must
746+
// propagate as null instead of panicking the whole scan with
747+
// "interface conversion: interface {} is nil, not []interface {}".
748+
{
749+
Code: "a = {a: [1,2]}; [1,2,3].containsAll(a['b'])",
750+
Expectation: nil,
751+
},
752+
{
753+
Code: "a = {a: [1,2]}; [1,2,3].containsNone(a['b'])",
754+
Expectation: nil,
755+
},
756+
{
757+
Code: "a = {a: [1,2]}; [1,2,3] - a['b']",
758+
Expectation: nil,
759+
},
760+
// Regression: containsAll of an empty (typed, non-null) list is
761+
// vacuously satisfied, so the compiled `== []` check is true. The
762+
// mondoo-linux-security su-restriction check relies on this for
763+
// `groups.containsAll(suRestrictedGroups)` when no group is configured
764+
// and suRestrictedGroups is an empty []string.
765+
{
766+
Code: `a = ["x", "y"]; ["wheel", "sudo"].containsAll(a.where(false))`,
767+
ResultIndex: 1, Expectation: true,
768+
},
744769
{
745770
Code: "['a','b'] != /c/",
746771
ResultIndex: 0, Expectation: true,

0 commit comments

Comments
Ā (0)