From 00f80df6c71b4802c9b596ed5fe3e97503fa5888 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Wed, 1 Jul 2026 15:09:23 -0300 Subject: [PATCH 1/2] Fix panic in GetClientConfig with null agent options config (#47388) --- changes/47388-getclientconfig-nil-map-panic | 1 + server/service/osquery.go | 7 +++ server/service/osquery_test.go | 47 +++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 changes/47388-getclientconfig-nil-map-panic diff --git a/changes/47388-getclientconfig-nil-map-panic b/changes/47388-getclientconfig-nil-map-panic new file mode 100644 index 00000000000..6562294f464 --- /dev/null +++ b/changes/47388-getclientconfig-nil-map-panic @@ -0,0 +1 @@ +- Fixed a server panic ("assignment to entry in nil map") when a host checked in for its osquery config while its agent options had a null `config`. diff --git a/server/service/osquery.go b/server/service/osquery.go index 267430054d8..4c4b704f17c 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -415,6 +415,13 @@ func (svc *Service) GetClientConfig(ctx context.Context) (map[string]interface{} if err != nil { return nil, newOsqueryError("internal error: parse base configuration: " + err.Error()) } + if config == nil { + // Unmarshaling the JSON literal `null` (e.g. agent options with + // "config": null) sets the map to nil rather than leaving it empty. + // Re-initialize so later assignments (e.g. config["packs"]) don't + // panic with "assignment to entry in nil map". + config = make(map[string]interface{}) + } } packConfig := fleet.Packs{} diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 8785c1b2f59..40b31698540 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -215,6 +215,53 @@ func TestGetClientConfig(t *testing.T) { ) } +// TestGetClientConfigNullConfig is a regression test for a panic +// ("assignment to entry in nil map") that occurred when a host's agent options +// had a null "config" and the host also had packs/scheduled queries. See +// https://github.com/fleetdm/fleet/issues/47388. +func TestGetClientConfigNullConfig(t *testing.T) { + ds := new(mock.Store) + + ds.TeamAgentOptionsFunc = func(ctx context.Context, teamID uint) (*json.RawMessage, error) { + return nil, nil + } + ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) { + return []*fleet.Pack{{ID: 1, Name: "pack_by_label"}}, nil + } + ds.ListScheduledQueriesInPackFunc = func(ctx context.Context, pid uint) (fleet.ScheduledQueryList, error) { + fals := false + return []*fleet.ScheduledQuery{ + {Name: "time", Query: "select * from time", Interval: 30, Removed: &fals}, + }, nil + } + ds.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint, hostID *uint, queryReportsDisabled bool) ([]*fleet.Query, error) { + return nil, nil + } + // Global agent options with a null config. This unmarshals into a nil map, + // which previously caused a panic once packs were added to the config. + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{AgentOptions: ptr.RawMessage(json.RawMessage(`{"config":null}`))}, nil + } + ds.UpdateHostFunc = func(ctx context.Context, host *fleet.Host) error { + return nil + } + + svc, ctx := newTestService(t, ds, nil, nil) + ctx = hostctx.NewContext(ctx, &fleet.Host{ID: 1}) + + conf, err := svc.GetClientConfig(ctx) + require.NoError(t, err) + assert.JSONEq(t, `{ + "pack_by_label": { + "queries":{ + "time":{"query":"select * from time","interval":30,"removed":false} + } + } + }`, + string(conf["packs"].(json.RawMessage)), + ) +} + func TestAgentOptionsForHost(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) From 02560450bd0f69453aafd35198ce458958e49333 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Wed, 1 Jul 2026 15:23:36 -0300 Subject: [PATCH 2/2] Fix lints --- server/service/osquery.go | 2 +- server/service/osquery_test.go | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/server/service/osquery.go b/server/service/osquery.go index 4c4b704f17c..f63be74a2c5 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -420,7 +420,7 @@ func (svc *Service) GetClientConfig(ctx context.Context) (map[string]interface{} // "config": null) sets the map to nil rather than leaving it empty. // Re-initialize so later assignments (e.g. config["packs"]) don't // panic with "assignment to entry in nil map". - config = make(map[string]interface{}) + config = make(map[string]any) } } diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 40b31698540..3469f56f720 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -229,9 +229,8 @@ func TestGetClientConfigNullConfig(t *testing.T) { return []*fleet.Pack{{ID: 1, Name: "pack_by_label"}}, nil } ds.ListScheduledQueriesInPackFunc = func(ctx context.Context, pid uint) (fleet.ScheduledQueryList, error) { - fals := false return []*fleet.ScheduledQuery{ - {Name: "time", Query: "select * from time", Interval: 30, Removed: &fals}, + {Name: "time", Query: "select * from time", Interval: 30, Removed: new(false)}, }, nil } ds.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint, hostID *uint, queryReportsDisabled bool) ([]*fleet.Query, error) { @@ -240,7 +239,7 @@ func TestGetClientConfigNullConfig(t *testing.T) { // Global agent options with a null config. This unmarshals into a nil map, // which previously caused a panic once packs were added to the config. ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { - return &fleet.AppConfig{AgentOptions: ptr.RawMessage(json.RawMessage(`{"config":null}`))}, nil + return &fleet.AppConfig{AgentOptions: new(json.RawMessage(`{"config":null}`))}, nil } ds.UpdateHostFunc = func(ctx context.Context, host *fleet.Host) error { return nil