state: avoid nil deref in registration handlers when old user is missing

Mirror the guard from HandleNodeFromPreAuthKey in HandleNodeFromAuthPath.
Both functions log the old user's name in the "different user" branch
when an existing NodeStore entry under the same machine key belongs to
another user. UserView.Name dereferences the backing User pointer
unconditionally, so when the cached node was loaded with a non-nil
UserID but a nil User (Preload join missed the row, or upstream code
left the snapshot in that shape), the log call panics with a nil-pointer
dereference at hscontrol/types/types_view.go:97.

The panic is caught by the http2 server's runHandler for the noise
control plane, so the process keeps running but every retry produces a
new panic — production has observed bursts of ~1.9k panics per hour
during a tailscaled reconnect loop. The gRPC/OIDC entry has no equivalent
recover and would surface the panic to the caller.

Guard both call sites with oldUser.Valid() and fall back to an empty
old-user name when the pointer is nil. The "Creating new node for
different user" log line still includes the existing node ID, hostname,
machine key, and new user, so operator visibility is preserved.

Add reproduction tests for both handlers seeding the orphan shape
directly into NodeStore via PutNodeInStoreForTest.

Co-Authored-By: Kristoffer Dalby <kristoffer@dalby.cc>
This commit is contained in:
SAY-5 2026-05-05 05:48:28 +00:00 committed by Kristoffer Dalby
parent 9482cdf590
commit 01e548e030
2 changed files with 125 additions and 2 deletions

View file

@ -1920,10 +1920,15 @@ func (s *State) HandleNodeFromAuthPath(
} else if existingNodeOwnedByOtherUser {
oldUser := existingNodeAnyUser.User()
oldUserName := ""
if oldUser.Valid() {
oldUserName = oldUser.Name()
}
logger.Info().
Str(zf.ExistingNodeName, existingNodeAnyUser.Hostname()).
Uint64(zf.ExistingNodeID, existingNodeAnyUser.ID().Uint64()).
Str(zf.OldUser, oldUser.Name()).
Str(zf.OldUser, oldUserName).
Msg("Creating new node for different user (same machine key exists for another user)")
finalNode, err = s.createNewNodeFromAuth(
@ -2223,7 +2228,12 @@ func (s *State) HandleNodeFromPreAuthKey(
if belongsToDifferentUser {
// Node exists but belongs to a different user.
// Create a new node for the new user (do not transfer).
oldUserName := existingNodeAnyUser.User().Name()
oldUser := existingNodeAnyUser.User()
oldUserName := ""
if oldUser.Valid() {
oldUserName = oldUser.Name()
}
log.Info().
Caller().