facebook / facebook/folly

Potential Issue in SharedMutex Implementation

Open
#2,280 1 comment 1 reaction 0 assignees View on GitHub
Dominant language
C++
Stars
30.5k
Forks
5.9k
PR merge metrics
No merged PRs in 30d

Description

I've been working with a simplified TLA+ model to verify the behavior of SharedMutex. During this process, I encountered an unexpected issue.

## TLA+ Model and Configuration

```tla
---------------------------- MODULE LockModel ----------------------------
EXTENDS Naturals, Sequences, FiniteSets, TLC

\* Constants
CONSTANTS NumExclusiveLockCallers, NumInlineSharedLockCallers, NumDeferredSharedLockCallers, MaxDeferredReadersAllocated

\* States
CONSTANTS Initialized, ExclusiveCountSet, DeferredSharedSlotsChecked, ZeroInlineSharedCountWaited, ExclusiveLockAcquired, ExclusiveLockReleased
CONSTANTS SharedLockAcquired, SharedLockReleased, DeferredSharedSlotSet

\* Variables
VARIABLES exclusiveCount, inlineSharedCount, deferredSharedSlots
VARIABLES exclusiveLockState, exclusiveLockCheckDeferredSharedSlotIndex
VARIABLES inlineSharedLockState, deferredSharedLockState

vars == <>

\* TypeOK ensures the variables are of correct types
TypeOK == /\ exclusiveCount \in 0..1
/\ inlineSharedCount \in Nat
/\ deferredSharedSlots \in [1..MaxDeferredReadersAllocated -> BOOLEAN]
/\ exclusiveLockState \in [1..NumExclusiveLockCallers -> {Initialized, ExclusiveCountSet, DeferredSharedSlotsChecked, ZeroInlineSharedCountWaited, ExclusiveLockAcquired, ExclusiveLockReleased}]
/\ exclusiveLockCheckDeferredSharedSlotIndex \in [1..NumExclusiveLockCallers -> 1..MaxDeferredReadersAllocated]
/\ inlineSharedLockState \in [1..NumInlineSharedLockCallers -> {Initialized, SharedLockAcquired, SharedLockReleased}]
/\ deferredSharedLockState \in [1..NumDeferredSharedLockCallers -> {Initialized, DeferredSharedSlotSet, SharedLockAcquired, SharedLockReleased}]

\* Initial state
Init == /\ exclusiveCount = 0
/\ inlineSharedCount = 0
/\ deferredSharedSlots = [i \in 1..MaxDeferredReadersAllocated |-> FALSE]
/\ exclusiveLockState = [i \in 1..NumExclusiveLockCallers |-> Initialized]
/\ exclusiveLockCheckDeferredSharedSlotIndex = [i \in 1..NumExclusiveLockCallers |-> 1]
/\ inlineSharedLockState = [i \in 1..NumInlineSharedLockCallers |-> Initialized]
/\ deferredSharedLockState = [i \in 1..NumDeferredSharedLockCallers |-> Initialized]

\* Exclusive Lock Operations
SetExclusiveCount(caller) ==
/\ exclusiveLockState[caller] = Initialized
/\ exclusiveCount = 0
/\ exclusiveCount' = 1
/\ exclusiveLockState' = [exclusiveLockState EXCEPT ![caller] = ExclusiveCountSet]
/\ UNCHANGED <>

CheckDeferredSharedSlotsOp(caller) ==
LET index == exclusiveLockCheckDeferredSharedSlotIndex[caller] IN
/\ exclusiveLockState[caller] = ExclusiveCountSet
/\ ~deferredSharedSlots[index]
/\ IF index = MaxDeferredReadersAllocated
THEN /\ exclusiveLockState' = [exclusiveLockState EXCEPT ![caller] = DeferredSharedSlotsChecked]
/\ UNCHANGED <>
ELSE /\ exclusiveLockCheckDeferredSharedSlotIndex' = [exclusiveLockCheckDeferredSharedSlotIndex EXCEPT ![caller] = index + 1]
/\ UNCHANGED <>
/\ UNCHANGED <>

WaitForZeroInlineSharedCountOp(caller) ==
/\ exclusiveLockState[caller] = DeferredSharedSlotsChecked
/\ inlineSharedCount = 0
/\ exclusiveLockState' = [exclusiveLockState EXCEPT ![caller] = ExclusiveLockAcquired]
/\ UNCHANGED <>

ReleaseExclusiveLock(caller) ==
/\ exclusiveLockState[caller] = ExclusiveLockAcquired
/\ exclusiveCount' = 0
/\ exclusiveLockState' = [exclusiveLockState EXCEPT ![caller] = ExclusiveLockReleased]
/\ UNCHANGED <>

\* Inline Shared Lock Operations
InlineSharedLockOp(caller) ==
/\ inlineSharedLockState[caller] = Initialized
/\ exclusiveCount = 0
/\ inlineSharedCount' = inlineSharedCount + 1
/\ inlineSharedLockState' = [inlineSharedLockState EXCEPT ![caller] = SharedLockAcquired]
/\ UNCHANGED <>

ReleaseInlineSharedLock(caller) ==
/\ inlineSharedLockState[caller] = SharedLockAcquired
/\ inlineSharedCount' = inlineSharedCount - 1
/\ inlineSharedLockState' = [inlineSharedLockState EXCEPT ![caller] = SharedLockReleased]
/\ UNCHANGED <>

\* Deferred Shared Lock Operations
SetDeferredSharedSlotsOp(caller) ==
/\ deferredSharedLockState[caller] = Initialized
/\ \E i \in {j \in 1..MaxDeferredReadersAllocated: ~deferredSharedSlots[j]}:
/\ deferredSharedSlots' = [deferredSharedSlots EXCEPT ![i] = TRUE]
/\ deferredSharedLockState' = [deferredSharedLockState EXCEPT ![caller] = DeferredSharedSlotSet]
/\ UNCHANGED <>

CheckZeroExclusiveCountOp(caller) ==
/\ deferredSharedLockState[caller] = DeferredSharedSlotSet
/\ IF exclusiveCount = 0
THEN /\ deferredSharedLockState' = [deferredSharedLockState EXCEPT ![caller] = SharedLockAcquired]
/\ UNCHANGED deferredSharedSlots
ELSE /\ \E i \in {j \in 1..MaxDeferredReadersAllocated: deferredSharedSlots[j]}:
deferredSharedSlots' = [deferredSharedSlots EXCEPT ![i] = FALSE]
/\ deferredSharedLockState' = [deferredSharedLockState EXCEPT ![caller] = Initialized]
/\ UNCHANGED <>

ReleaseDeferredSharedLock(caller) ==
/\ deferredSharedLockState[caller] = SharedLockAcquired
/\ \E i \in {j \in 1..MaxDeferredReadersAllocated: deferredSharedSlots[j]}:
deferredSharedSlots' = [deferredSharedSlots EXCEPT ![i] = FALSE]
/\ deferredSharedLockState' = [deferredSharedLockState EXCEPT ![caller] = SharedLockReleased]
/\ UNCHANGED <>

\* Next operation
Next == \/ \E caller \in 1..NumExclusiveLockCallers: SetExclusiveCount(caller) \/ CheckDeferredSharedSlotsOp(caller) \/ WaitForZeroInlineSharedCountOp(caller) \/ ReleaseExclusiveLock(caller)
\/ \E caller \in 1..NumInlineSharedLockCallers: InlineSharedLockOp(caller) \/ ReleaseInlineSharedLock(caller)
\/ \E caller \in 1..NumDeferredSharedLockCallers: SetDeferredSharedSlotsOp(caller) \/ CheckZeroExclusiveCountOp(caller) \/ ReleaseDeferredSharedLock(caller)

\* Specification
Spec == Init /\ [][Next]_vars /\ WF_vars(Next)

\* Liveness Property
EventuallyReleased == <>(
/\ \A caller \in 1..NumExclusiveLockCallers: exclusiveLockState[caller] = ExclusiveLockReleased
/\ \A caller \in 1..NumInlineSharedLockCallers: inlineSharedLockState[caller] = SharedLockReleased
/\ \A caller \in 1..NumDeferredSharedLockCallers: deferredSharedLockState[caller] = SharedLockReleased)

THEOREM SpecIsTypeOK == Spec => TypeOK
THEOREM SpecIsEventuallyReleased == Spec => EventuallyReleased

ExclusiveLockMutualExclusion == \A caller1, caller2 \in 1..NumExclusiveLockCallers:
(exclusiveLockState[caller1] = ExclusiveLockAcquired) /\ (exclusiveLockState[caller2] = ExclusiveLockAcquired) => caller1 = caller2
THEOREM SpecIsExclusiveLockMutualExclusion == Spec => ExclusiveLockMutualExclusion

ExclusiveLockPreventsShared == \A caller \in 1..NumExclusiveLockCallers:
exclusiveLockState[caller] = ExclusiveLockAcquired =>
/\ \A c \in 1..NumInlineSharedLockCallers: inlineSharedLockState[c] # SharedLockAcquired
/\ \A c \in 1..NumDeferredSharedLockCallers: deferredSharedLockState[c] # SharedLockAcquired
THEOREM SpecIsExclusiveLockPreventsShared == Spec => ExclusiveLockPreventsShared
=============================================================================
```

```cfg
CONSTANTS
NumExclusiveLockCallers = 3
NumInlineSharedLockCallers = 3
NumDeferredSharedLockCallers = 3
MaxDeferredReadersAllocated = 5

Initialized = Initialized
ExclusiveCountSet = ExclusiveCountSet
DeferredSharedSlotsChecked = DeferredSharedSlotsChecked
ZeroInlineSharedCountWaited = ZeroInlineSharedCountWaited
ExclusiveLockAcquired = ExclusiveLockAcquired
ExclusiveLockReleased = ExclusiveLockReleased
SharedLockAcquired = SharedLockAcquired
SharedLockReleased = SharedLockReleased
DeferredSharedSlotSet = DeferredSharedSlotSet

SPECIFICATION
Spec

INVARIANTS
TypeOK
ExclusiveLockMutualExclusion
ExclusiveLockPreventsShared

PROPERTY
\* EventuallyReleased
```

When running the command `tlc LockModel.tla -config LockModel.cfg -deadlock`, TLC reports the following error:

```text
Error: Invariant ExclusiveLockPreventsShared is violated.
Error: The behavior up to this point is:
State 1:
/\ inlineSharedLockState = <>
/\ exclusiveLockCheckDeferredSharedSlotIndex = <<1, 1, 1>>
/\ exclusiveLockState = <>
/\ deferredSharedLockState = <>
/\ deferredSharedSlots = <>
/\ inlineSharedCount = 0
/\ exclusiveCount = 0

State 2:
/\ inlineSharedLockState = <>
/\ exclusiveLockCheckDeferredSharedSlotIndex = <<1, 1, 1>>
/\ exclusiveLockState = <>
/\ deferredSharedLockState = <>
/\ deferredSharedSlots = <>
/\ inlineSharedCount = 0
/\ exclusiveCount = 0

State 3:
/\ inlineSharedLockState = <>
/\ exclusiveLockCheckDeferredSharedSlotIndex = <<1, 1, 1>>
/\ exclusiveLockState = <>
/\ deferredSharedLockState = <>
/\ deferredSharedSlots = <>
/\ inlineSharedCount = 0
/\ exclusiveCount = 0

State 4:
/\ inlineSharedLockState = <>
/\ exclusiveLockCheckDeferredSharedSlotIndex = <<1, 1, 1>>
/\ exclusiveLockState = <>
/\ deferredSharedLockState = <>
/\ deferredSharedSlots = <>
/\ inlineSharedCount = 0
/\ exclusiveCount = 1

State 5:
/\ inlineSharedLockState = <>
/\ exclusiveLockCheckDeferredSharedSlotIndex = <<2, 1, 1>>
/\ exclusiveLockState = <>
/\ deferredSharedLockState = <>
/\ deferredSharedSlots = <>
/\ inlineSharedCount = 0
/\ exclusiveCount = 1

State 6:
/\ inlineSharedLockState = <>
/\ exclusiveLockCheckDeferredSharedSlotIndex = <<2, 1, 1>>
/\ exclusiveLockState = <>
/\ deferredSharedLockState = <>
/\ deferredSharedSlots = <>
/\ inlineSharedCount = 0
/\ exclusiveCount = 1

State 7:
/\ inlineSharedLockState = <>
/\ exclusiveLockCheckDeferredSharedSlotIndex = <<2, 1, 1>>
/\ exclusiveLockState = <>
/\ deferredSharedLockState = <>
/\ deferredSharedSlots = <>
/\ inlineSharedCount = 0
/\ exclusiveCount = 1

State 8:
/\ inlineSharedLockState = <>
/\ exclusiveLockCheckDeferredSharedSlotIndex = <<3, 1, 1>>
/\ exclusiveLockState = <>
/\ deferredSharedLockState = <>
/\ deferredSharedSlots = <>
/\ inlineSharedCount = 0
/\ exclusiveCount = 1

State 9:
/\ inlineSharedLockState = <>
/\ exclusiveLockCheckDeferredSharedSlotIndex = <<4, 1, 1>>
/\ exclusiveLockState = <>
/\ deferredSharedLockState = <>
/\ deferredSharedSlots = <>
/\ inlineSharedCount = 0
/\ exclusiveCount = 1

State 10:
/\ inlineSharedLockState = <>
/\ exclusiveLockCheckDeferredSharedSlotIndex = <<5, 1, 1>>
/\ exclusiveLockState = <>
/\ deferredSharedLockState = <>
/\ deferredSharedSlots = <>
/\ inlineSharedCount = 0
/\ exclusiveCount = 1

State 11:
/\ inlineSharedLockState = <>
/\ exclusiveLockCheckDeferredSharedSlotIndex = <<5, 1, 1>>
/\ exclusiveLockState = <>
/\ deferredSharedLockState = <>
/\ deferredSharedSlots = <>
/\ inlineSharedCount = 0
/\ exclusiveCount = 1

State 12:
/\ inlineSharedLockState = <>
/\ exclusiveLockCheckDeferredSharedSlotIndex = <<5, 1, 1>>
/\ exclusiveLockState = <>
/\ deferredSharedLockState = <>
/\ deferredSharedSlots = <>
/\ inlineSharedCount = 0
/\ exclusiveCount = 1

845561 states generated, 123007 distinct states found, 26576 states left on queue.
The depth of the complete state graph search is 12.
```

## Code Path Analysis

The issue seems to occur under the following conditions:

The caller attempts to acquire an exclusive lock at:
1. [SharedMutex.h#L1082](https://github.com/facebook/folly/blob/b59f99e724af79721190cf8d06be62b10c72f8e9/folly/SharedMutex.h#L1082).
2. It checks whether all deferred readers are clear. If they are, `lockExclusiveImpl` returns `true`, indicating the exclusive lock is held: [SharedMutex.h#L1262](https://github.com/facebook/folly/blob/b59f99e724af79721190cf8d06be62b10c72f8e9/folly/SharedMutex.h#L1262).

The problem arises because the check for deferred readers is not atomic. It's possible for a caller attempting to acquire a read lock to manipulate the deferred slots. Specifically, setting slot 2, encountering an exclusive lock, and then releasing slot 1, effectively moving slot 2 to slot 1, which might bypass `applyDeferredReaders`.

## Uncertainty

The entire locking mechanism is quite complex for me, and I'm not certain if the TLA+ error is due to a misunderstanding on my part or if it indicates a genuine bug. I would appreciate the opportunity to discuss this further. Thank you for your attention.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.