CNA-BUG-164: HttpNotificationChannel's listener reads listenSocket_ unsynchronised while Close resets it, and spins without backoff on a persistent accept error
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.
listenSocket_ is a plain int that the worker reads for every accept while Close closes the descriptor and writes -1 from another thread, a data race; a persistent accept failure such as EMFILE makes the worker loop at full speed.
- Identifier
CNA-BUG-164- Category
- Bug
- Subsystem
- Networking & gamer services
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Severity
- Low (a triage suggestion, not a project priority)
- Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- HttpNotificationChannel::Close() and the channel's internal listener thread
Expected behaviour
Close is designed to run while the worker is blocked in accept (its comment says the shutdown call is what wakes it), and listening_ is a std::atomic<bool> for that reason. The other state the two threads share should be synchronised as well, and a failure the worker cannot recover from should not consume a core.
Actual behaviour at TARGET
listenSocket_ is an int. Listen reads it for every accept, while Close, on another thread, closes the descriptor and writes -1 with no lock or atomic: a data race, undefined behaviour under the C++ memory model. In practice a worker between its while (listening_) check and accept can use the old number after it was closed, and if another thread has meanwhile been given the same descriptor number, the worker calls accept on that unrelated descriptor. Separately, when accept fails while listening_ is still true (EMFILE or ENFILE when descriptors run out, ENOBUFS), Listen continues at once; EMFILE leaves the pending connection queued, so the loop spins at full speed until the condition clears. Not part of this entry: Open's plain if (listening_) check, which is not a compare-exchange; the header promises no thread safety for Open, and the Development page tells callers to keep Open and Close on one owning thread.
Source locations
modules/phone/src/HttpNotificationChannel.cpp— Listen (accept on listenSocket_, continue on failure), Close (closes the descriptor, then writes -1)modules/phone/include/Microsoft/Phone/Notification/HttpNotificationChannel.hpp— listenSocket_ declared as a plain int beside the atomic listening_
Evidence
Checked by reading HttpNotificationChannel.cpp at 009d40f5; the file is unchanged since the earlier recorded pin. Not executed.
Focused reproduction
No focused reproduction is known. Nothing has been invented here; the evidence above is what exists.
Current tests
Every notification test ends with Close running while the worker is blocked in accept, which is exactly the racy path, but no ThreadSanitizer run of the phone suites is recorded, and descriptor exhaustion is not tested.
Regression test
Run HttpNotificationChannelTest.* under ThreadSanitizer (it should report the race today); make listenSocket_ atomic or stop the worker through a wake-up descriptor before closing the listener; and add a test that lowers RLIMIT_NOFILE and checks that the worker backs off instead of spinning.
Blast radius
Every channel at Close (formal undefined behaviour, rarely visible); the busy loop only under descriptor or kernel buffer exhaustion.
Workaround
No workaround is known.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Maintainer workflow
- Thread and callback map
- Known issues
- Bug index