@@ -55,11 +55,12 @@ func sendIsAuthenticated(ctx context.Context, client authd.PAMClient, sessionID
5555 if st := status .Convert (err ); st .Code () == codes .Canceled {
5656 // Note that this error is only the client-side error, so being here doesn't
5757 // mean the cancellation on broker side is fully completed.
58-
59- // Wait for the cancellation requests to have been delivered and actually handled.
60- // The multiplier can be increased to avoid that we return the cancelled event too
61- // early, but it implies slowing down the UI responses.
62- <- time .After (cancellationWait * 3 )
58+ // We still wait briefly so that the CancelIsAuthenticated D-Bus call (sent
59+ // by the broker layer when ctx is cancelled) has time to arrive at the broker
60+ // after the IsAuthenticated call — not before it. Serialisation of
61+ // back-to-back IsAuthenticated calls for the same session is handled
62+ // server-side, so we no longer need a longer delay here.
63+ <- time .After (cancellationWait )
6364
6465 return isAuthenticatedResultReceived {
6566 access : auth .Cancelled ,
@@ -138,9 +139,18 @@ type authenticationModel struct {
138139 errorMsg string
139140}
140141
142+ // authTracker serialises IsAuthenticated calls and supports cancellation.
143+ //
144+ // At most one authentication goroutine is in flight at a time. A goroutine
145+ // that arrives while another is running blocks until the running one finishes.
146+ // cancelAndWait() cancels any in-flight goroutine and bumps the generation
147+ // counter so that any goroutine that is waiting (or has just been woken) knows
148+ // it has been superseded and must abort without making the RPC.
141149type authTracker struct {
142- cancelFunc func ()
143- cond * sync.Cond
150+ mu sync.Mutex
151+ generation uint64 // incremented by cancelAndWait; goroutines abort if theirs is stale
152+ cancelFunc func () // cancels the context of the current in-flight RPC; nil when idle
153+ done chan struct {} // closed by the goroutine when it exits; nil when idle
144154}
145155
146156// startAuthentication signals that the authentication model can start
@@ -174,7 +184,7 @@ func newAuthenticationModel(client authd.PAMClient, clientType PamClientType, mo
174184 client : client ,
175185 clientType : clientType ,
176186 mode : mode ,
177- authTracker : & authTracker {cond : sync . NewCond ( & sync. Mutex {}) },
187+ authTracker : & authTracker {},
178188 }
179189}
180190
@@ -293,7 +303,12 @@ func (m authenticationModel) Update(msg tea.Msg) (authModel authenticationModel,
293303 clientType := m .clientType
294304 currentLayout := m .currentLayout
295305 return m , func () tea.Msg {
296- authTracker .waitAndStart (cancelFunc )
306+ // waitForSlot blocks until no other auth is in flight, then registers
307+ // us as the active goroutine for this generation. It returns false
308+ // if we have been superseded by a cancelAndWait() call and must abort.
309+ if ! authTracker .waitForSlot (cancelFunc ) {
310+ return nil
311+ }
297312
298313 secret , hasSecret := msg .item .(* authd.IARequest_AuthenticationData_Secret )
299314 if hasSecret && clientType == Gdm && currentLayout == layouts .NewPassword {
@@ -332,7 +347,7 @@ func (m authenticationModel) Update(msg tea.Msg) (authModel authenticationModel,
332347 if msg .access != auth .Next && msg .access != auth .Retry {
333348 m .currentModel = nil
334349 }
335- m .authTracker .reset ()
350+ m .authTracker .finish ()
336351 }()
337352
338353 var authMsg string
@@ -550,43 +565,65 @@ func (authData *isAuthenticatedRequestedSend) encryptSecretIfPresent(publicKey *
550565 return & secret .Secret , nil
551566}
552567
553- // wait waits for the current authentication to be completed.
554- func (at * authTracker ) wait () {
555- at .cond .L .Lock ()
556- defer at .cond .L .Unlock ()
557-
558- for at .cancelFunc != nil {
559- at .cond .Wait ()
568+ // waitForSlot blocks until no other authentication goroutine is in flight,
569+ // then registers itself as the active goroutine. It returns true when the
570+ // caller should proceed with the RPC, or false when a cancelAndWait() call
571+ // has superseded this goroutine and the caller must abort.
572+ //
573+ // The generation counter is the key to correctness: cancelAndWait() bumps it
574+ // under the lock before signalling, so any goroutine that wakes up afterwards
575+ // will see a stale generation and abort — with no window for a race.
576+ func (at * authTracker ) waitForSlot (cancelFunc func ()) bool {
577+ at .mu .Lock ()
578+ gen := at .generation
579+ // Wait until the previous auth goroutine has finished.
580+ for at .done != nil {
581+ done := at .done
582+ at .mu .Unlock ()
583+ <- done
584+ at .mu .Lock ()
585+ }
586+ // If cancelAndWait() was called while we were waiting (or before we even
587+ // started), our generation is stale — abort without making any RPC.
588+ if at .generation != gen {
589+ at .mu .Unlock ()
590+ return false
560591 }
592+ // Register ourselves as the active goroutine.
593+ at .cancelFunc = cancelFunc
594+ at .done = make (chan struct {})
595+ at .mu .Unlock ()
596+ return true
561597}
562598
563- // waitAndStart waits for the current authentication to be completed and
564- // marks the authentication as in progress.
565- func (at * authTracker ) waitAndStart (cancelFunc func ()) {
566- at .cond .L .Lock ()
567- defer at .cond .L .Unlock ()
568-
569- for at .cancelFunc != nil {
570- at .cond .Wait ()
599+ // finish marks the active authentication goroutine as done. It must be called
600+ // by every goroutine that received true from waitForSlot, regardless of whether
601+ // the RPC was actually made (e.g. even for newPasswordCheck detours).
602+ func (at * authTracker ) finish () {
603+ at .mu .Lock ()
604+ done := at .done
605+ at .cancelFunc = nil
606+ at .done = nil
607+ at .mu .Unlock ()
608+ if done != nil {
609+ close (done )
571610 }
572-
573- at .cancelFunc = cancelFunc
574611}
575612
613+ // cancelAndWait cancels the in-flight authentication (if any) and waits for
614+ // its goroutine to exit. After it returns, any goroutine currently blocked in
615+ // waitForSlot will also abort, because the generation counter was bumped.
576616func (at * authTracker ) cancelAndWait () {
577- at .cond .L .Lock ()
617+ at .mu .Lock ()
618+ at .generation ++
578619 cancelFunc := at .cancelFunc
579- at .cond .L .Unlock ()
580- if cancelFunc == nil {
581- return
582- }
583- cancelFunc ()
584- at .wait ()
585- }
620+ done := at .done
621+ at .mu .Unlock ()
586622
587- func (at * authTracker ) reset () {
588- at .cond .L .Lock ()
589- defer at .cond .L .Unlock ()
590- at .cancelFunc = nil
591- at .cond .Signal ()
623+ if cancelFunc != nil {
624+ cancelFunc ()
625+ }
626+ if done != nil {
627+ <- done
628+ }
592629}
0 commit comments