<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_value checks to operator->() / 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.
<optional>ASan annotations cause false-positive use-after-poison crashes in valid user code at clang-cl-O2Description
In MSVC 14.51 (VS 2026 18.6),
<optional>was instrumented with__asan_poison_memory_region/__asan_unpoison_memory_regioncalls (gated by_INSERT_OPTIONAL_ANNOTATION, auto-defined under__SANITIZE_ADDRESS__or clang's__has_feature(address_sanitizer)). When a non-trivially-destructibleoptional<T>is disengaged, its_Valuestorage is poisoned forsizeof(T)bytes; when it becomes engaged the storage is unpoisoned (_Optional_destruct_baseat<optional>lines 124, 134, 164, 186).This produces false-positive
use-after-poisonreports 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
_Valuewhile_Has_value == false." Butoptional::operator->()andoperator*()do not enforce that invariant in release builds (<optional>lines 427-440 and 228-258):In a typical ASan build neither
_MSVC_STL_HARDENING_OPTIONALnor_ITERATOR_DEBUG_LEVEL != 0holds, so the check is compiled out. The accessor unconditionally returns&_Value.When a user writes idiomatic guarded access:
at
-O2, the optimizer appears to produce code that reads from_Valuestorage of a disengaged optional despite the source-levelhas_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 fouroptional<>members:sandbox::HardenTokenIntegrityLevelPolicycallsSecurityDescriptor::FromHandle(token, kKernel, LABEL_SECURITY_INFORMATION). Onlysacl_gets engaged. The other three stay disengaged, so their_Valuestorage is poisoned by_Optional_destruct_base's default constructor.Subsequent
SecurityDescriptor::WriteToHandle->SetSecurityDescriptorreads all four members:DWORD error = set_sd(..., UnwrapSid(sd.owner()), UnwrapSid(sd.group()), UnwrapAcl(sd.dacl()), UnwrapAcl(sd.sacl()));where the
UnwrapSid/UnwrapAclhelpers check engagement first:This is correct C++ - we never call
acl->get()on an empty optional. But with optimization enabled, ASan fires:The repeating
f7 f7 f7 00is exactly fouroptional<>storages: three disengaged (poisoned) + one engaged, in the order owner/group/dacl/sacl. The[f7]is offset 64 of theSecurityDescriptor, corresponding to the start ofdacl_._Value(the first disengaged optional whose storage was read).Workaround
-D_DISABLE_OPTIONAL_ANNOTATIONcleanly 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 triggerslld-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
/Odwhile leaving the optional annotations active eliminates the use-after-poison. At-O2the crash reproduces immediately.This is consistent with the optimizer reordering or speculatively executing loads from disengaged
optional<T>::_Valuestorage past thehas_value()guard that protects them in the source.Possible fixes (Microsoft side)
_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.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
-fsanitize=address -O2 -Oy-_MSVC_STL_HARDENING=1Standalone reduction
We attempted to reduce to a minimal
.cppthat triggers the samef7pattern 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.