`<optional>` ASan annotations cause false-positive use-after-poison crashes in valid user code at clang-cl `-O2`
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 11.2k
- Forks
- 1.7k
- Avg merge
- 4d 15h
- Merged PRs (30d)
- 22
Description
<optional> ASan annotations cause false-positive use-after-poison crashes in valid user code at clang-cl -O2
Description
In MSVC 14.51 (VS 2026 18.6), <optional> was instrumented with __asan_poison_memory_region / __asan_unpoison_memory_region calls (gated by _INSERT_OPTIONAL_ANNOTATION, auto-defined under __SANITIZE_ADDRESS__ or clang's __has_feature(address_sanitizer)). When a non-trivially-destructible optional<T> is disengaged, its _Value storage is poisoned for sizeof(T) bytes; when it becomes engaged the storage is unpoisoned (_Optional_destruct_base at <optional> lines 124, 134, 164, 186).
This produces false-positive use-after-poison reports on perfectly valid C++ at clang-cl -O2 -fsanitize=address. We reproduce this every Windows ASan CI run when building Firefox against VS 2026 18.6.
The contract issue
The annotation creates an invariant: "do not read _Value while _Has_value == false." But optional::operator->() and operator*() do not enforce that invariant in release builds (<optional> lines 427-440 and 228-258):
_NODISCARD constexpr const _Ty* operator->() const noexcept {
#if _MSVC_STL_HARDENING_OPTIONAL || _ITERATOR_DEBUG_LEVEL != 0
_STL_VERIFY(this->_Has_value, "operator->() called on empty optional");
#endif
return _STD addressof(this->_Value);
}
In a typical ASan build neither _MSVC_STL_HARDENING_OPTIONAL nor _ITERATOR_DEBUG_LEVEL != 0 holds, so the check is compiled out. The accessor unconditionally returns &_Value.
When a user writes idiomatic guarded access:
if (opt) {
return opt->some_member;
}
at -O2, the optimizer appears to produce code that reads from _Value storage of a disengaged optional despite the source-level has_value() guard. The read hits the poisoned shadow and ASan fires. The crash does not reproduce at /Od (see "Optimization sensitivity" below).
Concrete failure
Chromium's base::win::SecurityDescriptor (used in Firefox via the in-tree Chromium sandbox copy) holds four optional<> members:
absl::optional<Sid> owner_;
absl::optional<Sid> group_;
absl::optional<AccessControlList> dacl_;
absl::optional<AccessControlList> sacl_;
sandbox::HardenTokenIntegrityLevelPolicy calls SecurityDescriptor::FromHandle(token, kKernel, LABEL_SECURITY_INFORMATION). Only sacl_ gets engaged. The other three stay disengaged, so their _Value storage is poisoned by _Optional_destruct_base's default constructor.
Subsequent SecurityDescriptor::WriteToHandle -> SetSecurityDescriptor reads all four members:
DWORD error = set_sd(..., UnwrapSid(sd.owner()), UnwrapSid(sd.group()),
UnwrapAcl(sd.dacl()), UnwrapAcl(sd.sacl()));
where the UnwrapSid / UnwrapAcl helpers check engagement first:
PACL UnwrapAcl(const absl::optional<AccessControlList>& acl) {
if (!acl) return nullptr;
return acl->get();
}
This is correct C++ - we never call acl->get() on an empty optional. But with optimization enabled, ASan fires:
==NNNN==ERROR: AddressSanitizer: use-after-poison on address 0x... at pc 0x...
READ of size 8 at 0x... thread T0
#0 base::win::SetSecurityDescriptor security_descriptor.cc:166
#1 SecurityDescriptor::WriteToHandle ...
#2 sandbox::HardenTokenIntegrityLevelPolicy ...
Shadow bytes around the buggy address:
...
=>0x...: f7 f7 f7 00 f7 f7 f7 00 f7 f7 f7 00 [f7] 00 ...
The repeating f7 f7 f7 00 is exactly four optional<> storages: three disengaged (poisoned) + one engaged, in the order owner/group/dacl/sacl. The [f7] is offset 64 of the SecurityDescriptor, corresponding to the start of dacl_._Value (the first disengaged optional whose storage was read).
Workaround
-D_DISABLE_OPTIONAL_ANNOTATION cleanly disables the feature (via the #undefs in __msvc_sanitizer_annotate_container.hpp). However, because per-subtree scoping is not viable due to the #pragma detect_mismatch("annotate_optional", ...) enforcement in the annotation header (partial application triggers lld-link: error: /failifmismatch: mismatch detected for 'annotate_optional'), the workaround has to be applied to every TU in the binary, losing optional ASan coverage everywhere just to suppress a bug whose footprint is narrow.
Optimization sensitivity
The crash is optimization-sensitive. Building the same code at /Od while leaving the optional annotations active eliminates the use-after-poison. At -O2 the crash reproduces immediately.
This is consistent with the optimizer reordering or speculatively executing loads from disengaged optional<T>::_Value storage past the has_value() guard that protects them in the source.
Possible fixes (Microsoft side)
- Add
_Has_valuechecks tooperator->()/operator*()unconditionally. Cost is one predictable branch per dereference (always taken in valid code); the optimizer can use this as a barrier preventing speculative reads through the accessor. This is the most natural fix. - Drop the annotations entirely.
- Use a barrier intrinsic in the accessors to constrain the optimizer.
Related
microsoft/STL#6276 is a similar but distinct bug in <vector> / <string> annotations - the same "STL annotation creates an invariant the accessors don't enforce" family.
Environment
- VS 2026 18.6.0 / MSVC 14.51.36231
- clang-cl 20 (Mozilla's prebuilt)
- x64 Windows ASan build,
-fsanitize=address -O2 -Oy- _MSVC_STL_HARDENING=1
Standalone reduction
We attempted to reduce to a minimal .cpp that triggers the same f7 pattern but could not - the bug appears to depend on the optimizer's specific inlining / scheduling decisions in a larger binary. The Firefox CI failure is reliable (~100% on Windows ASan tests) and we can provide build artifacts and full ASan logs on request.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with at _Optional_destruct_base and operator->()/operator*() around the cited lines, then inspect __msvc_sanitizer_annotate_container.hpp and the Firefox ASan failure path. Reproduce the clang-cl -O2 case using the supplied environment and logs or build artifacts. Done means valid guarded optional access no longer triggers use-after-poison while the intended annotation behavior remains covered.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100