ipn/store: make WriteState(id, nil) delete key instead of adding nil entry (#19920)

All StateStore implementations store a nil value in the cache map when WriteState is called with a nil byte slice instead of deleting the key. This causes ReadState to return (nil, nil) instead of (nil, ErrStateNotExist), since the key is still present in the map.

This breaks reset-auth in Windows, Linux, and Android, and the node can't log back in without manually editing the state file. (macOS uses a different state store)
DeleteProfile, DeleteAllProfilesForUser, setUnattendedModeAsConfigured are impacted but don't seem to break because the deleted keys are not reread.

This deletes the key from the cache instead.

Fixes tailscale/corp#42477

Signed-off-by: kari-ts <kari@tailscale.com>
This commit is contained in:
kari-ts
2026-05-29 11:22:14 -07:00
committed by GitHub
parent 3d5102090f
commit 7355116c05
9 changed files with 194 additions and 10 deletions
+30
View File
@@ -8683,3 +8683,33 @@ func TestPeerRoutesCGNATCollapse(t *testing.T) {
t.Errorf("with subnet: got %v; want %v", got, want)
}
}
func TestResetAuthClearsMachineKey(t *testing.T) {
store := new(mem.Store)
// Write a machine key to the store.
machineKey := key.NewMachine()
keyText, _ := machineKey.MarshalText()
if err := store.WriteState(ipn.MachineKeyStateKey, keyText); err != nil {
t.Fatal(err)
}
// Verify key is readable.
if bs, err := store.ReadState(ipn.MachineKeyStateKey); err != nil {
t.Fatalf("ReadState before clear: %v", err)
} else if len(bs) == 0 {
t.Fatal("machine key is empty before clear")
}
// Clear the key the same way ResetAuth does.
if err := ipn.WriteState(store, ipn.MachineKeyStateKey, nil); err != nil {
t.Fatal(err)
}
// Verify key is gone. It should return ErrStateNotExist,
// not nil/nil which would cause initMachineKeyLocked to
// fail with "invalid key ... doesn't have expected type prefix".
if _, err := store.ReadState(ipn.MachineKeyStateKey); err != ipn.ErrStateNotExist {
t.Fatalf("ReadState after clear: got err %v, want ErrStateNotExist", err)
}
}
+44
View File
@@ -1185,3 +1185,47 @@ func (k *failingHardwareAttestationKey) UnmarshalJSON([]byte) error {
k.unmarshalCalled = true
return errors.New("failed to unmarshal attestation key!")
}
func TestDeleteProfileClearsState(t *testing.T) {
store := new(mem.Store)
pm, err := newProfileManagerWithGOOS(store, logger.Discard, health.NewTracker(eventbustest.NewBus(t)), "linux")
if err != nil {
t.Fatal(err)
}
pm.SetCurrentUserID("user1")
pm.SwitchToNewProfile()
p := pm.CurrentPrefs().AsStruct()
p.Persist = &persist.Persist{
NodeID: "node1",
PrivateNodeKey: key.NewNode(),
UserProfile: tailcfg.UserProfile{
ID: 1,
LoginName: "user@example.com",
},
}
if err := pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil {
t.Fatal(err)
}
profileKey := pm.currentProfile.Key()
if profileKey == "" {
t.Fatal("profile key is empty")
}
// Verify profile state exists in store.
if _, err := store.ReadState(profileKey); err != nil {
t.Fatalf("ReadState before delete: %v", err)
}
profileID := pm.currentProfile.ID()
if err := pm.DeleteProfile(profileID); err != nil {
t.Fatal(err)
}
// Verify profile state is deleted from store.
if _, err := store.ReadState(profileKey); err != ipn.ErrStateNotExist {
t.Fatalf("ReadState after delete: got err %v, want ErrStateNotExist", err)
}
}