Other(refactor): Sort safe.Mutex types before locking to prevent deadlocks

This change implements safe.Mutex and safe.RWMutex, which wrap the
sync.Mutex and sync.RWMutex types and are assigned a globally unique
integer ID. The safe.Lock and safe.RLock methods sort the mutexes
by this integer ID before locking to ensure that locks for a given
set of mutexes are always performed in the same order, avoiding
deadlocks.
This commit is contained in:
James Houlahan
2022-10-27 01:21:40 +02:00
parent 5a4f733518
commit d4da325e57
11 changed files with 133 additions and 61 deletions

View File

@ -70,7 +70,7 @@ func (user *User) handleUserEvent(_ context.Context, userEvent liteapi.User) err
})
return nil
}, &user.apiUserLock)
}, user.apiUserLock)
}
// handleAddressEvents handles the given address events.
@ -132,7 +132,7 @@ func (user *User) handleCreateAddressEvent(ctx context.Context, event liteapi.Ad
})
return nil
}, &user.apiAddrsLock, &user.updateChLock)
}, user.apiAddrsLock, user.updateChLock)
}
func (user *User) handleUpdateAddressEvent(_ context.Context, event liteapi.AddressEvent) error { //nolint:unparam
@ -150,7 +150,7 @@ func (user *User) handleUpdateAddressEvent(_ context.Context, event liteapi.Addr
})
return nil
}, &user.apiAddrsLock)
}, user.apiAddrsLock)
}
func (user *User) handleDeleteAddressEvent(_ context.Context, event liteapi.AddressEvent) error {
@ -174,7 +174,7 @@ func (user *User) handleDeleteAddressEvent(_ context.Context, event liteapi.Addr
})
return nil
}, &user.apiAddrsLock, &user.updateChLock)
}, user.apiAddrsLock, user.updateChLock)
}
// handleLabelEvents handles the given label events.
@ -220,7 +220,7 @@ func (user *User) handleCreateLabelEvent(_ context.Context, event liteapi.LabelE
})
return nil
}, &user.apiLabelsLock, &user.updateChLock)
}, user.apiLabelsLock, user.updateChLock)
}
func (user *User) handleUpdateLabelEvent(_ context.Context, event liteapi.LabelEvent) error { //nolint:unparam
@ -242,7 +242,7 @@ func (user *User) handleUpdateLabelEvent(_ context.Context, event liteapi.LabelE
})
return nil
}, &user.apiLabelsLock, &user.updateChLock)
}, user.apiLabelsLock, user.updateChLock)
}
func (user *User) handleDeleteLabelEvent(_ context.Context, event liteapi.LabelEvent) error { //nolint:unparam
@ -265,7 +265,7 @@ func (user *User) handleDeleteLabelEvent(_ context.Context, event liteapi.LabelE
})
return nil
}, &user.apiLabelsLock, &user.updateChLock)
}, user.apiLabelsLock, user.updateChLock)
}
// handleMessageEvents handles the given message events.
@ -307,7 +307,7 @@ func (user *User) handleCreateMessageEvent(ctx context.Context, event liteapi.Me
return nil
})
}, &user.apiUserLock, &user.apiAddrsLock, &user.updateChLock)
}, user.apiUserLock, user.apiAddrsLock, user.updateChLock)
}
func (user *User) handleUpdateMessageEvent(_ context.Context, event liteapi.MessageEvent) error { //nolint:unparam
@ -322,7 +322,7 @@ func (user *User) handleUpdateMessageEvent(_ context.Context, event liteapi.Mess
user.updateCh[event.Message.AddressID].Enqueue(update)
return nil
}, &user.updateChLock)
}, user.updateChLock)
}
func getMailboxName(label liteapi.Label) []string {

View File

@ -88,7 +88,7 @@ func (conn *imapConnector) GetMailbox(ctx context.Context, mailboxID imap.Mailbo
}
return toIMAPMailbox(mailbox, conn.flags, conn.permFlags, conn.attrs), nil
}, &conn.apiLabelsLock)
}, conn.apiLabelsLock)
}
// CreateMailbox creates a label with the given name.
@ -157,7 +157,7 @@ func (conn *imapConnector) createFolder(ctx context.Context, name []string) (ima
}
return toIMAPMailbox(label, conn.flags, conn.permFlags, conn.attrs), nil
}, &conn.apiLabelsLock)
}, conn.apiLabelsLock)
}
// UpdateMailboxName sets the name of the label with the given ID.
@ -232,7 +232,7 @@ func (conn *imapConnector) updateFolder(ctx context.Context, labelID imap.Mailbo
}
return nil
}, &conn.apiLabelsLock)
}, conn.apiLabelsLock)
}
// DeleteMailbox deletes the label with the given ID.
@ -350,7 +350,7 @@ func (conn *imapConnector) MarkMessagesFlagged(ctx context.Context, messageIDs [
func (conn *imapConnector) GetUpdates() <-chan imap.Update {
return safe.RLockRet(func() <-chan imap.Update {
return conn.updateCh[conn.addrID].GetChannel()
}, &conn.updateChLock)
}, conn.updateChLock)
}
// GetUIDValidity returns the default UID validity for this user.
@ -407,7 +407,7 @@ func (conn *imapConnector) importMessage(
return nil
})
}, &conn.apiUserLock, &conn.apiAddrsLock); err != nil {
}, conn.apiUserLock, conn.apiAddrsLock); err != nil {
return imap.Message{}, nil, err
}

View File

@ -110,7 +110,7 @@ func (user *User) sync(ctx context.Context) error {
return nil
})
}, &user.apiUserLock, &user.apiAddrsLock, &user.updateChLock)
}, user.apiUserLock, user.apiAddrsLock, user.updateChLock)
}
func syncLabels(ctx context.Context, client *liteapi.Client, updateCh ...*queue.QueuedChannel[imap.Update]) error {

View File

@ -59,16 +59,16 @@ type User struct {
sendHash *sendRecorder
apiUser liteapi.User
apiUserLock sync.RWMutex
apiUserLock safe.RWMutex
apiAddrs map[string]liteapi.Address
apiAddrsLock sync.RWMutex
apiAddrsLock safe.RWMutex
apiLabels map[string]liteapi.Label
apiLabelsLock sync.RWMutex
apiLabelsLock safe.RWMutex
updateCh map[string]*queue.QueuedChannel[imap.Update]
updateChLock sync.RWMutex
updateChLock safe.RWMutex
tasks *xsync.Group
abortable async.Abortable
@ -144,10 +144,17 @@ func New(
eventCh: queue.NewQueuedChannel[events.Event](0, 0),
sendHash: newSendRecorder(sendEntryExpiry),
apiUser: apiUser,
apiAddrs: groupBy(apiAddrs, func(addr liteapi.Address) string { return addr.ID }),
apiLabels: groupBy(apiLabels, func(label liteapi.Label) string { return label.ID }),
updateCh: updateCh,
apiUser: apiUser,
apiUserLock: safe.NewRWMutex(),
apiAddrs: groupBy(apiAddrs, func(addr liteapi.Address) string { return addr.ID }),
apiAddrsLock: safe.NewRWMutex(),
apiLabels: groupBy(apiLabels, func(label liteapi.Label) string { return label.ID }),
apiLabelsLock: safe.NewRWMutex(),
updateCh: updateCh,
updateChLock: safe.NewRWMutex(),
tasks: xsync.NewGroup(context.Background()),
@ -210,14 +217,14 @@ func New(
func (user *User) ID() string {
return safe.RLockRet(func() string {
return user.apiUser.ID
}, &user.apiUserLock)
}, user.apiUserLock)
}
// Name returns the user's username.
func (user *User) Name() string {
return safe.RLockRet(func() string {
return user.apiUser.Name
}, &user.apiUserLock)
}, user.apiUserLock)
}
// Match matches the given query against the user's username and email addresses.
@ -234,7 +241,7 @@ func (user *User) Match(query string) bool {
}
return false
}, &user.apiUserLock, &user.apiAddrsLock)
}, user.apiUserLock, user.apiAddrsLock)
}
// Emails returns all the user's email addresses via the callback.
@ -243,7 +250,7 @@ func (user *User) Emails() []string {
return xslices.Map(maps.Values(user.apiAddrs), func(addr liteapi.Address) string {
return addr.Email
})
}, &user.apiAddrsLock)
}, user.apiAddrsLock)
}
// GetAddressMode returns the user's current address mode.
@ -286,7 +293,7 @@ func (user *User) SetAddressMode(ctx context.Context, mode vault.AddressMode) er
}
return nil
}, &user.apiAddrsLock, &user.updateChLock)
}, user.apiAddrsLock, user.updateChLock)
}
// GetGluonIDs returns the users gluon IDs.
@ -323,14 +330,14 @@ func (user *User) BridgePass() []byte {
func (user *User) UsedSpace() int {
return safe.RLockRet(func() int {
return user.apiUser.UsedSpace
}, &user.apiUserLock)
}, user.apiUserLock)
}
// MaxSpace returns the amount of space the user can use on the API.
func (user *User) MaxSpace() int {
return safe.RLockRet(func() int {
return user.apiUser.MaxSpace
}, &user.apiUserLock)
}, user.apiUserLock)
}
// GetEventCh returns a channel which notifies of events happening to the user (such as deauth, address change).
@ -367,7 +374,7 @@ func (user *User) NewIMAPConnectors() (map[string]connector.Connector, error) {
}
return imapConn, nil
}, &user.apiAddrsLock)
}, user.apiAddrsLock)
}
// SendMail sends an email from the given address to the given recipients.
@ -483,7 +490,7 @@ func (user *User) SendMail(authID string, from string, to []string, r io.Reader)
return nil
})
}, &user.apiUserLock, &user.apiAddrsLock)
}, user.apiUserLock, user.apiAddrsLock)
}
// CheckAuth returns whether the given email and password can be used to authenticate over IMAP or SMTP with this user.
@ -506,7 +513,7 @@ func (user *User) CheckAuth(email string, password []byte) (string, error) {
}
return "", fmt.Errorf("invalid email")
}, &user.apiAddrsLock)
}, user.apiAddrsLock)
}
// OnStatusUp is called when the connection goes up.
@ -549,7 +556,7 @@ func (user *User) Close() {
for _, updateCh := range xslices.Unique(maps.Values(user.updateCh)) {
updateCh.CloseAndDiscardQueued()
}
}, &user.updateChLock)
}, user.updateChLock)
// Close the user's notify channel.
user.eventCh.CloseAndDiscardQueued()