cloneDeviceRecord shares empty (non-nil) Metadata map between original and clone #683
Labels
No labels
1week
2weeks
Failed compliance check
IP cameras
NATS
Possible security concern
Review effort 1/5
Review effort 2/5
Review effort 3/5
Review effort 4/5
Review effort 5/5
UI
aardvark
accessibility
amd64
api
arm64
auth
back-end
bgp
blog
bug
build
checkers
ci-cd
cleanup
cnpg
codex
core
dependencies
device-management
documentation
duplicate
dusk
ebpf
enhancement
eta 1d
eta 1hr
eta 3d
eta 3hr
feature
fieldsurvey
github_actions
go
good first issue
help wanted
invalid
javascript
k8s
log-collector
mapper
mtr
needs-triage
netflow
network-sweep
observability
oracle
otel
plug-in
proton
python
question
reddit
redhat
research
rperf
rperf-checker
rust
sdk
security
serviceradar-agent
serviceradar-agent-gateway
serviceradar-web
serviceradar-web-ng
siem
snmp
sysmon
topology
ubiquiti
wasm
wontfix
zen-engine
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
carverauto/serviceradar#683
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Imported from GitHub.
Original GitHub issue: #2145
Original author: @mfreeman451
Original URL: https://github.com/carverauto/serviceradar/issues/2145
Original created: 2025-12-16T05:16:25Z
Summary
cloneDeviceRecordinpkg/registry/device_store.gois used throughout the device registry to create defensive copies of device records, ensuring that callers can safely modify returned records without affecting the original stored data.DeviceRecordwith an empty (but not nil)Metadatamap, the function fails to deep-copy the map, causing both the original and clone to share the same underlying map reference.Metadatamap directly affect the original stored record'sMetadata, instead of the two records having independent metadata maps.Code with bug
The bug is at line 204 in
pkg/registry/device_store.go. Whensrc.Metadatais an empty map (length 0 but not nil), the conditionif len(src.Metadata) > 0evaluates to false, so the map is not cloned. Sincedst := *srccreates a shallow copy of the struct,dst.Metadatastill points to the same underlying map assrc.Metadata.Evidence
Example
Consider the following sequence:
DeviceRecordis stored in the registry withMetadata: map[string]string{}GetDeviceRecord(), which returns a cloneclone.Metadata["key1"] = "value1"GetDeviceRecord(), which returns another clonekey1 = "value1"even though they expect a fresh copyThis happens because:
cloneDeviceRecord(R)returns clone C1, but C1.Metadata still points to 0x1000 (not copied)cloneDeviceRecord(R)returns clone C2, but C2.Metadata still points to 0x1000Failing test
Test script
Test output
Running
go test -v -count=1 ./pkg/registry -run TestCloneDeviceRecord_EmptyMapIsolation:Running
go test -v -count=1 ./pkg/registry -run TestGetDeviceRecord_ModifyingCloneAffectsOriginal_EmptyMap:Running
go test -v -count=1 ./pkg/registry -run TestGetDeviceRecord_RaceCondition:Full context
The device registry maintains an in-memory cache of device records (
DeviceRegistry.devices map[string]*DeviceRecord). To provide defensive copying and prevent external callers from corrupting this internal state, all getter methods (GetDeviceRecord,FindDevicesByIP,FindDevicesByMAC,SnapshotRecords) usecloneDeviceRecordto return deep copies of records.Empty (but not nil)
Metadatamaps can be created in several ways:Direct initialization with empty maps: Throughout the codebase, there are many instances where code initializes metadata as
map[string]string{}(e.g., in tests atpkg/registry/registry_test.go:284,pkg/registry/service_device_test.go:297)Via
mergeMetadatafunction: Inpkg/registry/device_store_update.go:145-163, themergeMetadatafunction can produce empty maps when:destis nilsrchas entries but they all have empty/whitespace-only keysmergeMetadata(nil, map[string]string{"": "value"})returns an empty (non-nil) mapVia various update flows: The registry processes device updates through multiple integration sources (Armis, Netbox, sweep discovery), and metadata can start as an initialized empty map (e.g.,
pkg/registry/registry.go:1622,pkg/db/db.go:354)The bug affects any code path that:
GetDeviceRecord()MetadatamapKey affected call sites:
pkg/registry/device_store_update.go:34: Gets existing record, passes todeviceRecordFromUpdatewhich may mutate itpkg/registry/registry.go:1896: Public APIGetDevice()returns cloned recordspkg/registry/registry.go:1960: Search functionality retrieves and operates on cloned recordspkg/registry/registry.go:2044:FindRelatedDevices()retrieves cloned recordsWhy has this bug gone undetected?
This bug has remained undetected for several reasons:
Uncommon edge case: Empty (but not nil) metadata maps are less common than either nil maps or maps with content. Most device records in production have at least some metadata entries, which would trigger the cloning logic and work correctly.
Defensive pattern elsewhere: The codebase has defensive checks like
if update.Metadata == nil { update.Metadata = make(map[string]string) }(e.g.,pkg/registry/registry.go:1621-1623) before writing to metadata. This pattern masks the bug because it creates a new map before writing, preventing corruption of the shared empty map.Existing tests don't validate isolation: The existing test
TestUpsertAndGetDeviceRecordatpkg/registry/device_store_test.go:60-78does test mutation isolation, but it uses a record with non-empty metadata (metadata := map[string]string{"region": testRegionUSEast1}), so the cloning logic works correctly in that test.Timing-dependent manifestation: The bug only manifests when:
Map reference equality not tested: None of the existing tests verify that cloned records have independent map instances (by checking if modifications to one clone affect another), they only check value equality at a single point in time.
Recommended fix
Change the condition at line 204 in
pkg/registry/device_store.gofromif len(src.Metadata) > 0toif src.Metadata != nil:This ensures that any non-nil map (including empty maps) gets deep-copied, while truly nil maps remain nil in the clone.
The same issue likely affects the slice fields (
DiscoverySourcesandCapabilities), though in practice this is less problematic because Go slice headers are copied by value, and the append operations used in the code typically create new underlying arrays. However, for consistency and safety, these should also be fixed:Imported GitHub comment.
Original author: @mfreeman451
Original URL: https://github.com/carverauto/serviceradar/issues/2145#issuecomment-3663012986
Original created: 2025-12-17T00:20:13Z
closing as completed