Files
tailscale/ipn/store/mem/store_mem.go
T
kari-tsandGitHub 7355116c05 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>
2026-05-29 11:22:14 -07:00

92 lines
2.1 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package mem provides an in-memory ipn.StateStore implementation.
package mem
import (
"bytes"
"encoding/json"
"sync"
xmaps "golang.org/x/exp/maps"
"tailscale.com/ipn"
"tailscale.com/types/logger"
"tailscale.com/util/mak"
)
// New returns a new Store.
func New(logger.Logf, string) (ipn.StateStore, error) {
return new(Store), nil
}
// Store is an ipn.StateStore that keeps state in memory only.
type Store struct {
mu sync.Mutex
// +checklocks:mu
cache map[ipn.StateKey][]byte
}
func (s *Store) String() string { return "mem.Store" }
// ReadState implements the StateStore interface.
// It returns ipn.ErrStateNotExist if the state does not exist.
func (s *Store) ReadState(id ipn.StateKey) ([]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
bs, ok := s.cache[id]
if !ok {
return nil, ipn.ErrStateNotExist
}
return bs, nil
}
// WriteState implements the StateStore interface.
// It never returns an error.
func (s *Store) WriteState(id ipn.StateKey, bs []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.cache == nil {
s.cache = map[ipn.StateKey][]byte{}
}
if bs == nil {
delete(s.cache, id)
} else {
s.cache[id] = bytes.Clone(bs)
}
return nil
}
// LoadFromMap loads the in-memory cache from the provided map.
// Any existing content is cleared, and the provided map is
// copied into the cache.
func (s *Store) LoadFromMap(m map[string][]byte) {
s.mu.Lock()
defer s.mu.Unlock()
xmaps.Clear(s.cache)
for k, v := range m {
mak.Set(&s.cache, ipn.StateKey(k), v)
}
return
}
// LoadFromJSON attempts to unmarshal json content into the
// in-memory cache.
func (s *Store) LoadFromJSON(data []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
return json.Unmarshal(data, &s.cache)
}
// ExportToJSON exports the content of the cache to
// JSON formatted []byte.
func (s *Store) ExportToJSON() ([]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.cache) == 0 {
// Avoid "null" serialization.
return []byte("{}"), nil
}
return json.MarshalIndent(s.cache, "", " ")
}