Submit an issue View all issues Source
MIR-931

buildEntitySaveOps corrupts shared primary slice, dropping attrs on CreateEntity overwrite

Done Bug public
phinze phinze Opened Mar 30, 2026 Updated Mar 30, 2026

Summary

buildEntitySaveOps mutates the caller's primary slice via a shared backing array. When SetUpdatedAt appends db/entity.updated and re-sorts, attributes that sort after it alphabetically (e.g. db/type) get pushed past primary's length. On the CreateEntity + WithOverwrite path, the first call to buildEntitySaveOps (for the initial txn attempt) corrupts primary, then the overwrite re-uses it — persisting the entity without the dropped attrs.

Impact

Server crash-loops on boot for any cluster upgrading from a pre-short-id build to the short-id build (PR #696, c55ae16). Schema attribute entities (installed by schema.Apply) lose their entity/type attr, causing GetAttributeSchema to fail for every subsequent entity operation. Garden was down for ~30 minutes.

Root Cause

buildEntitySaveOps at pkg/entity/store.go:973:

entity.attrs = primary          // aliases the same backing array
entity.SetUpdatedAt(time.Now()) // appends + SortedAttrs reorders in-place

Since entity.attrs and primary share the same backing array, SortedAttrs shuffles elements beyond primary's length. When the create txn fails (entity already exists) and the overwrite path reuses primary, it's missing the displaced attrs.

Fix

One line: entity.attrs = slices.Clone(primary) in buildEntitySaveOps to break the alias.

Introduced in

PR #696 (c55ae16d) — the retry loop moved buildEntitySaveOps inside the loop, making primary reusable across attempts and exposing the aliasing bug. The pre-existing buildEntitySaveOps mutation was harmless when called only once.