Date: Thu, 15 Jan 2026 07:53:06 +0000
I think it would be better to submit this as an LWG issue if you want to pursue this. See How To Submit a New Issue / Defect Report : Standard C++<https://isocpp.org/std/submit-issue>.
Thanks,
F.v.S.
________________________________
From: Std-Proposals <std-proposals-bounces_at_[hidden]> on behalf of Ryan P. Nicholl via Std-Proposals <std-proposals_at_[hidden]>
Sent: Thursday, January 15, 2026 8:49
To: std-proposals <std-proposals_at_[hidden]>
Cc: Ryan P. Nicholl <rnicholl_at_[hidden]>
Subject: [std-proposals] Draft proposal for fixing undefined behavior in `atomic_notify_*` operations
I'm looking to circulate a draft for a defect report proposal to get feedback.
Defect Report: Well-defined behavior for atomic_notify_* on expired pointers
Document Number: DxxxxR0
Date: 2026-01-14
Audience: SG1 (Concurrency), LEWG (Library Evolution)
Project: ISO/IEC JTC1/SC22/WG21 14882:2020 (C++20)
Reply-to: Ryan P. Nicholl <rnicholl_at_[hidden]> (do I put my name here or should this go to the standard committee?)
1. Abstract
This paper proposes a defect report for C++20 to clarify the requirements for the non-member atomic notification functions (atomic_notify_one and atomic_notify_all). Specifically, it proposes clarifying that these functions remain well-defined even if the pointer provided refers to an object whose lifetime has ended or to an address where a new atomic object exists to which the pointer lacks provenance.
2. Motivation
In the implementation of custom user defined synchronization primitives similar to std::mutex or std::latch, using the C++20 <atomic> waiting and notifying facilities, a race condition would occur if the pointer is required to remain valid at the time of the call to atomic_notify_one or atomic_notify_all.
Consider a distinct implementation of a mutex. The unlock operation typically involves:
1. Atomically storing a "free" state to the atomic flag/integer.
2. Notifying waiting threads.
[http://localhost:63342/markdownPreview/1694423628/]
void unlock() {
state.store(0, std::memory_order_release);
std::atomic_notify_one(&state);
}
If a thread acquires the lock immediately after the store in step 1, it may proceed to destroy the mutex object while the unlocking thread is still executing step 2 (the notification).
However, the notification operation physically requires only the address to identify the wait queue; it does not need to access the object's value or metadata, and from my investigations it does not appear that any major implementation does this. Current implementations of std::latch rely on internal wake function implementations being safe even if the atomic object no longer exists.
If atomic_notify_* is treated as a standard function requiring a valid pointer, implementing safe, custom synchronization primitives becomes impossible without external coordination or UB.
It is unclear from the existing text whether these functions are safe to call on a now-expired pointer. This paper proposes adding clarification text standardizing the behavior of existing implementations allowing it.
This proposal seeks to make atomic_notify_one and atomic_notify_all safe to call with concurrently expiring pointers, allowing the program to remain well-defined even if the target atomic is concurrently destroyed.
3. Impact on the Standard
This proposal affects section 31 (Atomics) of the C++ Standard. It specifically modifies the semantics of non-member functions defined in [atomics.nonmembers] (referenced in the provided text as 31.9).
References to the provided text:
* 31.6 Waiting and notifying [atomics.waitnotify] defines the general terminology for these operations.
* 31.6 Waiting and notifying defines the general terminology for these operations.
* 31.7.1 Operations [atomics.ref.ops] defines the member functions notify_one and notify_all.
* 31.9 Non-member functions [atomics.nonmembers] defines the non-member functions themselves.
4. Proposed Wording
The following changes are relative to N4860 (C++20).
31.6 Waiting and notifying [atomics.waitnotify]
-(4) A call to an atomic waiting operation on an atomic object M is eligible to be unblocked by a call to an atomic notifying operation on a pointer to M if there exist side effects X and Y on M such that:
—(4.1) the atomic waiting operation has blocked after observing the result of X,
—(4.2) X precedes Y in the modification order of M, and
—(4.3) Y happens before the call to the atomic notifying operation
31.7.1 Operations [atomics.ref.ops]
void notify_one() const noexcept; -(25) Effects: Unblocks the execution of at least one atomic waiting operation on *ptr that is eligible to be unblocked (31.6) by this call, if any such atomic waiting operations exist. -(26) Remarks: This function is an atomic notifying operation (31.6) on the pointer to atomic object * ptr. void notify_all() const noexcept; -(27) Effects: Unblocks the execution of all atomic waiting operations on *ptr that are eligible to be unblocked (31.6) by this call. -(28) Remarks: This function is an atomic notifying operation (31.6) on the pointer to atomic object * ptr.
31.9 Non-member functions [atomics.nonmembers]
31.9 Non-member functions [atomics.nonmembers]
-(1) Except for atomic_notify_one and atomic_notify_all, a A non-member function template whose name matches the pattern atomic_f or the pattern atomic_f _- explicit invokes the member function f , with the value of the first parameter as the object expression and the values of the remaining parameters (if any) as the arguments of the member function call, in order. An argument for a parameter of type atomic::value_type* is dereferenced when passed to the member function call. If no such member function exists, the program is ill-formed.
-(2) [Note: The non-member functions enable programmers to write code that can be compiled as either C or C++, for example in a shared header file. — end note]
template<class T> void atomic_notify_one(atomic<T>*);
-(3) Effects: Unblocks the execution of at least one atomic waiting operation that is eligible to be unblocked (31.6) by this call, if any such atomic waiting operations exist.
-(4) If the pointer is no longer valid or the destination object is concurrently destroyed, it is unspecified whether any waiters on any atomic object at the same address are spuriously unblocked, but the program does not exhibit undefined behavior.
-(5) Remarks: This function is an atomic notifying operation (31.6).
template<class T> void atomic_notify_all(atomic<T>*);
-(6) Effects: Unblocks the execution of all atomic waiting operations that are eligible to be unblocked (31.6) by this call. -(7) If the pointer is no longer valid or the destination object is concurrently destroyed, it is unspecified whether any waiters on any atomic object at the same address are spuriously unblocked, but the program does not exhibit undefined behavior. -(8) Remarks: This function is an atomic notifying operation (31.6).
5. Rationale for "Unspecified" Unblocking
If the pointer is invalid (e.g., the ABA problem on the address where an object was destroyed and a new one created), the notification might inadvertently target the wait queue of the new object.
* We cannot guarantee the old waiters are woken (the queue might be gone).
* We cannot guarantee the new waiters are ignored (the address collision might map to the same kernel bucket).
Therefore, the side effects on waiters are unspecified, but the rigid requirement that the program must not exhibit UB is added. This ensures that custom synchronization primitives can be implemented safely without risking UB due to pointer validity issues.
Thanks,
F.v.S.
________________________________
From: Std-Proposals <std-proposals-bounces_at_[hidden]> on behalf of Ryan P. Nicholl via Std-Proposals <std-proposals_at_[hidden]>
Sent: Thursday, January 15, 2026 8:49
To: std-proposals <std-proposals_at_[hidden]>
Cc: Ryan P. Nicholl <rnicholl_at_[hidden]>
Subject: [std-proposals] Draft proposal for fixing undefined behavior in `atomic_notify_*` operations
I'm looking to circulate a draft for a defect report proposal to get feedback.
Defect Report: Well-defined behavior for atomic_notify_* on expired pointers
Document Number: DxxxxR0
Date: 2026-01-14
Audience: SG1 (Concurrency), LEWG (Library Evolution)
Project: ISO/IEC JTC1/SC22/WG21 14882:2020 (C++20)
Reply-to: Ryan P. Nicholl <rnicholl_at_[hidden]> (do I put my name here or should this go to the standard committee?)
1. Abstract
This paper proposes a defect report for C++20 to clarify the requirements for the non-member atomic notification functions (atomic_notify_one and atomic_notify_all). Specifically, it proposes clarifying that these functions remain well-defined even if the pointer provided refers to an object whose lifetime has ended or to an address where a new atomic object exists to which the pointer lacks provenance.
2. Motivation
In the implementation of custom user defined synchronization primitives similar to std::mutex or std::latch, using the C++20 <atomic> waiting and notifying facilities, a race condition would occur if the pointer is required to remain valid at the time of the call to atomic_notify_one or atomic_notify_all.
Consider a distinct implementation of a mutex. The unlock operation typically involves:
1. Atomically storing a "free" state to the atomic flag/integer.
2. Notifying waiting threads.
[http://localhost:63342/markdownPreview/1694423628/]
void unlock() {
state.store(0, std::memory_order_release);
std::atomic_notify_one(&state);
}
If a thread acquires the lock immediately after the store in step 1, it may proceed to destroy the mutex object while the unlocking thread is still executing step 2 (the notification).
However, the notification operation physically requires only the address to identify the wait queue; it does not need to access the object's value or metadata, and from my investigations it does not appear that any major implementation does this. Current implementations of std::latch rely on internal wake function implementations being safe even if the atomic object no longer exists.
If atomic_notify_* is treated as a standard function requiring a valid pointer, implementing safe, custom synchronization primitives becomes impossible without external coordination or UB.
It is unclear from the existing text whether these functions are safe to call on a now-expired pointer. This paper proposes adding clarification text standardizing the behavior of existing implementations allowing it.
This proposal seeks to make atomic_notify_one and atomic_notify_all safe to call with concurrently expiring pointers, allowing the program to remain well-defined even if the target atomic is concurrently destroyed.
3. Impact on the Standard
This proposal affects section 31 (Atomics) of the C++ Standard. It specifically modifies the semantics of non-member functions defined in [atomics.nonmembers] (referenced in the provided text as 31.9).
References to the provided text:
* 31.6 Waiting and notifying [atomics.waitnotify] defines the general terminology for these operations.
* 31.6 Waiting and notifying defines the general terminology for these operations.
* 31.7.1 Operations [atomics.ref.ops] defines the member functions notify_one and notify_all.
* 31.9 Non-member functions [atomics.nonmembers] defines the non-member functions themselves.
4. Proposed Wording
The following changes are relative to N4860 (C++20).
31.6 Waiting and notifying [atomics.waitnotify]
-(4) A call to an atomic waiting operation on an atomic object M is eligible to be unblocked by a call to an atomic notifying operation on a pointer to M if there exist side effects X and Y on M such that:
—(4.1) the atomic waiting operation has blocked after observing the result of X,
—(4.2) X precedes Y in the modification order of M, and
—(4.3) Y happens before the call to the atomic notifying operation
31.7.1 Operations [atomics.ref.ops]
void notify_one() const noexcept; -(25) Effects: Unblocks the execution of at least one atomic waiting operation on *ptr that is eligible to be unblocked (31.6) by this call, if any such atomic waiting operations exist. -(26) Remarks: This function is an atomic notifying operation (31.6) on the pointer to atomic object * ptr. void notify_all() const noexcept; -(27) Effects: Unblocks the execution of all atomic waiting operations on *ptr that are eligible to be unblocked (31.6) by this call. -(28) Remarks: This function is an atomic notifying operation (31.6) on the pointer to atomic object * ptr.
31.9 Non-member functions [atomics.nonmembers]
31.9 Non-member functions [atomics.nonmembers]
-(1) Except for atomic_notify_one and atomic_notify_all, a A non-member function template whose name matches the pattern atomic_f or the pattern atomic_f _- explicit invokes the member function f , with the value of the first parameter as the object expression and the values of the remaining parameters (if any) as the arguments of the member function call, in order. An argument for a parameter of type atomic::value_type* is dereferenced when passed to the member function call. If no such member function exists, the program is ill-formed.
-(2) [Note: The non-member functions enable programmers to write code that can be compiled as either C or C++, for example in a shared header file. — end note]
template<class T> void atomic_notify_one(atomic<T>*);
-(3) Effects: Unblocks the execution of at least one atomic waiting operation that is eligible to be unblocked (31.6) by this call, if any such atomic waiting operations exist.
-(4) If the pointer is no longer valid or the destination object is concurrently destroyed, it is unspecified whether any waiters on any atomic object at the same address are spuriously unblocked, but the program does not exhibit undefined behavior.
-(5) Remarks: This function is an atomic notifying operation (31.6).
template<class T> void atomic_notify_all(atomic<T>*);
-(6) Effects: Unblocks the execution of all atomic waiting operations that are eligible to be unblocked (31.6) by this call. -(7) If the pointer is no longer valid or the destination object is concurrently destroyed, it is unspecified whether any waiters on any atomic object at the same address are spuriously unblocked, but the program does not exhibit undefined behavior. -(8) Remarks: This function is an atomic notifying operation (31.6).
5. Rationale for "Unspecified" Unblocking
If the pointer is invalid (e.g., the ABA problem on the address where an object was destroyed and a new one created), the notification might inadvertently target the wait queue of the new object.
* We cannot guarantee the old waiters are woken (the queue might be gone).
* We cannot guarantee the new waiters are ignored (the address collision might map to the same kernel bucket).
Therefore, the side effects on waiters are unspecified, but the rigid requirement that the program must not exhibit UB is added. This ensures that custom synchronization primitives can be implemented safely without risking UB due to pointer validity issues.
-- Ryan P. Nicholl Tel: (678)-358-7765
Received on 2026-01-15 07:53:12
