Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 4 additions & 24 deletions internal/brokers/broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,26 +142,14 @@ func (b Broker) SelectAuthenticationMode(ctx context.Context, sessionID, authent
func (b Broker) IsAuthenticated(ctx context.Context, sessionID, authenticationData string) (access string, data string, err error) {
sessionID = b.parseSessionID(sessionID)

// monitor ctx in goroutine to call cancel
done := make(chan struct{})
go func() {
access, data, err = b.brokerer.IsAuthenticated(ctx, sessionID, authenticationData)
close(done)
}()

select {
case <-done:
if err != nil {
return "", "", err
}
case <-ctx.Done():
b.cancelIsAuthenticated(ctx, sessionID)
<-done
access, data, err = b.brokerer.IsAuthenticated(ctx, sessionID, authenticationData)
if err != nil {
return "", "", err
}

// Validate access authentication.
if !slices.Contains(auth.Replies, access) {
return "", "", fmt.Errorf("invalid access authentication key: %v", access)
return "", "", fmt.Errorf("invalid authentication reply: %v", access)
}

if data == "" {
Expand Down Expand Up @@ -223,14 +211,6 @@ func (b Broker) endSession(ctx context.Context, sessionID string) (err error) {
return b.brokerer.EndSession(ctx, sessionID)
}

// cancelIsAuthenticated calls the broker corresponding method.
// If the session does not have a pending IsAuthenticated call, this is a no-op.
//
// Even though this is a public method, it should only be interacted with through IsAuthenticated and ctx cancellation.
func (b Broker) cancelIsAuthenticated(ctx context.Context, sessionID string) {
b.brokerer.CancelIsAuthenticated(ctx, sessionID)
}

// UserPreCheck calls the broker corresponding method.
func (b Broker) UserPreCheck(ctx context.Context, username string) (userinfo string, err error) {
log.Debugf(context.TODO(), "Pre-checking user %q", username)
Expand Down
6 changes: 3 additions & 3 deletions internal/brokers/broker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ func TestIsAuthenticated(t *testing.T) {
cancelFirstCall bool
}{
"Successfully_authenticate": {sessionID: "success"},
"Successfully_authenticate_after_cancelling_first_call": {sessionID: "ia_second_call", secondCall: true},
"Successfully_authenticate_after_cancelling_first_call": {sessionID: "ia_second_call", secondCall: true, cancelFirstCall: true},
"Denies_authentication_when_broker_times_out": {sessionID: "ia_timeout"},
"Adds_default_groups_even_if_broker_did_not_set_them": {sessionID: "ia_info_empty_groups"},
"No_error_when_auth.Next_and_no_data": {sessionID: "ia_next"},
Expand All @@ -236,7 +236,7 @@ func TestIsAuthenticated(t *testing.T) {
"Error_when_broker_returns_data_on_auth.Cancelled": {sessionID: "ia_cancelled_with_data"},
"Error_when_broker_returns_no_data_on_auth.Denied": {sessionID: "ia_denied_without_data"},
"Error_when_broker_returns_no_data_on_auth.Retry": {sessionID: "ia_retry_without_data"},
"Error_when_calling_IsAuthenticated_a_second_time_without_cancelling": {sessionID: "ia_second_call", secondCall: true, cancelFirstCall: true},
"Error_when_calling_IsAuthenticated_a_second_time_without_cancelling": {sessionID: "ia_second_call", secondCall: true},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
Expand Down Expand Up @@ -264,7 +264,7 @@ func TestIsAuthenticated(t *testing.T) {
time.Sleep(time.Second)

if tc.secondCall {
if !tc.cancelFirstCall {
if tc.cancelFirstCall {
cancel()
<-done
}
Expand Down
17 changes: 12 additions & 5 deletions internal/brokers/dbusbroker.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"

"github.com/godbus/dbus/v5"
"github.com/ubuntu/authd/internal/brokers/auth"
"github.com/ubuntu/authd/internal/services/errmessages"
"github.com/ubuntu/authd/log"
"github.com/ubuntu/decorate"
Expand Down Expand Up @@ -98,9 +99,16 @@ func (b dbusBroker) SelectAuthenticationMode(ctx context.Context, sessionID, aut
}

// IsAuthenticated calls the corresponding method on the broker bus and returns the user information and access.
func (b dbusBroker) IsAuthenticated(_ context.Context, sessionID, authenticationData string) (access, data string, err error) {
// We don’t want to cancel the context when the parent call is cancelled.
call, err := b.call(context.Background(), "IsAuthenticated", sessionID, authenticationData)
func (b dbusBroker) IsAuthenticated(ctx context.Context, sessionID, authenticationData string) (access, data string, err error) {
call, err := b.call(ctx, "IsAuthenticated", sessionID, authenticationData)
if errors.Is(err, context.Canceled) {
// In contrast to gRPC, D-Bus does not support cancelling ongoing method calls through context cancellation.
// BusObject.CallWithContext only aborts the call on the client side when the context is cancelled.
// So if that happens, we need to call CancelIsAuthenticated to also cancel the ongoing call on the broker side,
// to avoid that the broker continuous its work to authenticate the user.
b.CancelIsAuthenticated(context.Background(), sessionID)
return auth.Cancelled, "", nil
Comment on lines +109 to +110
Copy link
Member

@denisonbarbosa denisonbarbosa Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this create a race? CancelIsAuthenticated is not a blocking call, so (also considering that the "wait" was removed on the IsAuthenticated function) we can end up returning to PAM without the broker being "ready" to handle a new call, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CancelIsAuthenticated is not a blocking call

Really? It also uses BusObject.CallWithContext from the dbus package, which according to the documentation waits for a reply.

Copy link
Member

@denisonbarbosa denisonbarbosa Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the dbus call returns, but if you look at the CancelIsAuthenticated func on the broker side, it's not waiting for the cancellation to be done, it just requests a cancel and then the IsAuthenticated func that returns whether it was cancelled or not. Did it make sense? It's not easy to explain

}
if err != nil {
return "", "", err
}
Expand All @@ -121,8 +129,7 @@ func (b dbusBroker) EndSession(ctx context.Context, sessionID string) (err error

// CancelIsAuthenticated calls the corresponding method on the broker bus.
func (b dbusBroker) CancelIsAuthenticated(ctx context.Context, sessionID string) {
// We don’t want to cancel the context when the parent call is cancelled.
if _, err := b.call(context.Background(), "CancelIsAuthenticated", sessionID); err != nil {
if _, err := b.call(ctx, "CancelIsAuthenticated", sessionID); err != nil {
log.Errorf(ctx, "could not cancel IsAuthenticated call for session %q: %v", sessionID, err)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FIRST CALL:
access:
data:
err: invalid access authentication key: invalid
err: invalid authentication reply: invalid
5 changes: 5 additions & 0 deletions internal/services/errmessages/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ type ToDisplayError struct {
error
}

// Unwrap returns the underlying error.
func (e ToDisplayError) Unwrap() error {
return e.error
}

// NewToDisplayError returns a new ErrorToDisplay.
func NewToDisplayError(err error) error {
return ToDisplayError{err}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FIRST CALL:
access:
msg:
err: can't check authentication: invalid access authentication key: invalid
err: can't check authentication: invalid authentication reply: invalid
15 changes: 15 additions & 0 deletions pam/main-exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import (
"fmt"
"os"
"runtime"
"syscall"
"time"

"github.com/msteinert/pam/v2"
"github.com/ubuntu/authd/log"
"github.com/ubuntu/authd/pam/internal/dbusmodule"
"golang.org/x/sys/unix"
)

var (
Expand All @@ -25,6 +27,19 @@ func init() {
// calling the dbus services from the process and so that the module PID
// check won't fail.
runtime.LockOSThread()

// Ask the kernel to send SIGTERM if the parent dies
if err := unix.Prctl(unix.PR_SET_PDEATHSIG, uintptr(syscall.SIGTERM), 0, 0, 0); err != nil {
log.Errorf(context.Background(), "failed to set PDEATHSIG: %v", err)
os.Exit(1)
Comment on lines +32 to +34
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was wondering to use the same too... But maybe we can handle it also client-side, on the dbus side... Let me check that too.

However this is fine.

The changes on broker instead I'd hold since I had some refactoring there for another branch, and that side is quite delicate sadly (as we may cancel too early).

}

// Check if parent is still alive
ppid := unix.Getppid()
if ppid == 1 {
log.Error(context.Background(), "parent is already gone; exiting")
os.Exit(1)
}
}

func mainFunc() error {
Expand Down
Loading