Written 2026-08-10 on branch claude/remediation-batch-1804-namespace-b1yjh5. Audit numbering is
frozen at 364; nothing in this document creates an SR-AUD-* identifier. Post-audit defects
found here take ordinary ticket numbers only.
audit/AUDIT_FINDINGS_INDEX.md was re-parsed from scratch (all 364 rows, grouped by owning
module). The decomposition measured today is:
| Bucket | Count |
|---|---|
remediated |
161 |
confirmed (plain) |
150 |
confirmed (design-complete) |
53 |
| total | 364 |
Correction to the inherited brief. The handoff prompt for this batch stated 160 remediated /
154 confirmed / 50 design-complete. That is not what the index says. The numbers above are the
authoritative recount and they agree exactly with the previous batch's own handoff paragraph at the
top of NEXT.md (161 / 203 confirmed of which 53 design-complete). The prompt's triple was stale;
the index and NEXT.md agree with each other and with this recount.
Candidates are units with no review plan and no remediated finding.
| Candidate | Open | high | med | Actionable here | Blocked | Approval-gated | System data present | Memory risk | Public-input exposure | Cohesion |
|---|---|---|---|---|---|---|---|---|---|---|
time-zone |
7 | 0 | 7 | 7 | 0 | 0 | yes — 499 TZif zones installed | none | medium (zone ids) | high — 4 headers, 2 bodies, 4 suites, 114 tests |
globalization |
7 | 1 | 6 | ~2 | 0 | 1 (Calendar abstract, 82 tests pin the shape) |
no — needs ICU collation/grapheme data | high (TSan-confirmed culture race) | high | medium |
xml-linq |
4 | 1 | 3 | 3 | 1 — its high is CCF-019 (#1899/#1894) | 0 | n/a | high, but that half is the blocked one | high | high |
net-network-information |
3 | 0 | 3 | 3 | 0 | 0 | partial | low | medium | small; adjacent to blocked #1962 |
core |
72 | 9 | 59 | many | — | several | n/a | mixed | high | not a namespace — already carved by seven CCF-* plans, 47 findings remediated |
io |
7 | 0 | 7 | — | — | — | — | — | — | already has docs/SystemIONamespaceReviewPlan.md |
time-zone is confirmed as the correct next unit, and for a stronger reason than the previous
batch could give: every one of its seven findings was reproduced in this container before any
code was changed (§6). It is the only remaining candidate that is simultaneously the largest by
open count among genuinely unreviewed namespaces, zero-high, zero-blocked, zero-approval-gated, and
fully decidable from data that is physically present.
At the #2176 review checkpoint the .NET reference tree was unavailable. That blocked message-text parity questions and nothing else in this namespace: five of the seven findings were self-consistency or validation questions (an out parameter that is not written, a factory with no validation, an equality/hash pair that disagree with each other, a property that contradicts its own doc-comment), and the audit reports for the other two carry direct current-.NET probe results recorded at audit time. The remaining reference questions were recorded instead of guessed and were subsequently answered and closed by #2186; §17 now preserves those outcomes.
Physical component SharpRuntime::TimeZone → target sharp_runtime_time_zone,
PUBLIC_DEPENDENCIES Core.Base, one edge in the 41-module / 92-edge graph.
| File | Lines | Role |
|---|---|---|
modules/time-zone/include/System/TimeZoneInfo.hpp |
719 | the whole public surface: TimeZoneInfo + nested TransitionTime + nested AdjustmentRule |
modules/time-zone/include/System/TimeZone.hpp |
62 | legacy abstract TimeZone + CurrentTimeZone() |
modules/time-zone/include/System/TimeZoneNotFoundException.hpp |
22 | lookup failure |
modules/time-zone/include/System/InvalidTimeZoneException.hpp |
60 | corrupt zone data; non-TZif files now throw it (#2186) |
modules/time-zone/src/System/TimeZoneInfo.cpp |
309 | POSIX/Windows lookup, Local(), the CLDR IANA↔Windows table |
modules/time-zone/src/System/TimeZone.cpp |
39 | the CurrentTimeZone() adapter |
modules/time-zone/tests/System/TimeZoneInfoTests.cpp |
608 | 75 tests |
modules/time-zone/tests/System/AdjustmentRuleTests.cpp |
205 | 19 tests |
modules/time-zone/tests/System/TimeZoneTests.cpp |
71 | 8 tests |
modules/time-zone/tests/System/TimeZoneNotFoundExceptionTests.cpp |
45 | 12 tests |
Baseline: SharpRuntimeTests_TimeZone = 114 tests, 114 passing.
There is no TZif parser in this module. Resolution is stat() on
/usr/share/zoneinfo/<id> followed by setenv("TZ", id) + tzset() + localtime_r(); libc owns
every byte of TZif parsing. The brief's "TZif parser safety" matrix therefore does not apply as
written, and §14 records what replaced it.
getIdProperty, getDisplayNameProperty, getStandardNameProperty, getDaylightNameProperty,
getBaseUtcOffsetProperty, getSupportsDaylightSavingTimeProperty, getHasIanaIdProperty.
IsDaylightSavingTime(DateTime), IsDaylightSavingTime(DateTimeOffset), GetUtcOffset(DateTime),
GetUtcOffset(DateTimeOffset), IsAmbiguousTime(DateTime), IsAmbiguousTime(DateTimeOffset),
IsInvalidTime(DateTime), GetAmbiguousTimeOffsets(DateTime),
GetAmbiguousTimeOffsets(DateTimeOffset), GetAdjustmentRules(), ConvertTimeToUtc(DateTime),
HasSameRules, Equals, GetHashCode, ToString, operator==, operator!=.
Utc(), Local(), ClearCachedData(), FindSystemTimeZoneById, TryFindSystemTimeZoneById,
GetSystemTimeZones(), GetSystemTimeZones(bool), CreateCustomTimeZone,
ConvertTimeBySystemTimeZoneId ×3, ConvertTime ×3, ConvertTimeFromUtc, ConvertTimeToUtc,
TryConvertIanaIdToWindowsId, TryConvertWindowsIdToIanaId.
getTimeOfDayProperty, getMonthProperty, getWeekProperty, getDayProperty,
getDayOfWeekProperty, getIsFixedDateRuleProperty, validateTimeOfDay, CreateFixedDateRule,
CreateFloatingDateRule, Equals, GetHashCode, operator==, operator!=.
getDateStartProperty, getDateEndProperty, getDaylightDeltaProperty,
getDaylightTransitionStartProperty, getDaylightTransitionEndProperty,
getBaseUtcOffsetDeltaProperty, getNoDaylightTransitionsProperty,
getHasDaylightSavingProperty, GetHashCode, Equals, operator==, operator!=,
CreateAdjustmentRule (5-arg and 6-arg).
getStandardNameProperty, getDaylightNameProperty, GetUtcOffset(DateTime),
IsDaylightSavingTime(DateTime), CurrentTimeZone().
Serialization (ToSerializedString / FromSerializedString), CreateCustomTimeZone overloads
taking adjustment rules, TimeZoneInfo::Utc/Local as fields, AdjustmentRule public
constructor. Recorded in §18.
| Finding | Severity | Disposition | Ticket | Compatible? |
|---|---|---|---|---|
SR-AUD-223 legacy CurrentTimeZone freezes one offset and never reports DST |
medium | compatible implementation | #2182 | yes — file-local adapter, no public shape touched |
SR-AUD-224 failed TryFind leaves a stale zone in the out parameter |
medium | compatible implementation | #2177 | yes — one inline body |
SR-AUD-225 CreateCustomTimeZone accepts empty ids and out-of-range offsets |
medium | compatible implementation | #2178 | yes — new rejections, audit-probe-evidenced |
SR-AUD-226 AdjustmentRule accepts dateEnd < dateStart |
medium | compatible implementation | #2179 | yes — new rejection, audit-probe-evidenced |
| SR-AUD-227 equality is case-sensitive while the hash is case-insensitive | medium | compatible implementation | #2180 | yes — also repairs an internal contract breach |
SR-AUD-229 IANA lookup records the current offset as BaseUtcOffset |
medium | compatible implementation | #2181 | yes — the implementation contradicts its own doc-comment |
SR-AUD-228 HasSameRules reduces every rule set to offset + a bool |
medium | accepted permanent deviation | #2185 (closed after the proposed sampled-rule design proved insufficient) | no — faithful repair requires out-of-scope TZif rule parsing |
No finding disappears and no finding is reclassified as a false premise. All seven are real and all seven reproduce (§6).
Post-audit defects found during this review, with ordinary ticket numbers and no SR-AUD-*
identifier:
| Ticket | Defect |
|---|---|
| #2183 | FindSystemTimeZoneById accepts the 7 non-TZif regular files shipped inside /usr/share/zoneinfo, and accepts malformed identifiers (America//New_York, ./America/New_York, an embedded NUL) |
| #2184 | the TZ save/restore window is not exception-safe, and an empty-but-set TZ is deleted rather than restored |
| #2186 | subsequently closed five reference-parity questions: three repairs and two already-correct outcomes |
Every correction below is a measurement, recorded in build-probe/2176_probe*.log.
The audit records BaseUtcOffset. The same snapshot-of-time(nullptr) also feeds
StandardName and DaylightName, which are written from tm_zone at that instant, so both
names take the daylight abbreviation for a zone queried during its DST period and the two names
are always identical:
America/New_York port: base=-240m std=EDT dst=EDT truth: std EST -300m, daylight EDT -240m
Europe/Prague port: base=+120m std=CEST dst=CEST truth: std CET +60m, daylight CEST +120m
Europe/Dublin port: base= +60m std=IST dst=IST truth: std GMT 0m, daylight IST +60m
Scale, measured over every installed zone (2176_probe4_allzones.log): 499 TZif zones,
158 observe DST somewhere in 2025, and 141 of 499 report the wrong BaseUtcOffset today
(2026-08-10, northern summer). The remaining 17 are the southern-hemisphere DST zones, which are
correct today and wrong in January — the month-dependence the audit names, demonstrated in both
directions from one run.
TimeZoneInfo.hpp:364-369 already says "DST transitions are not modelled; this is always the
standard offset." The POSIX implementation did not deliver the standard offset. The original
repair made it agree with the documented contract. The later #2418 ripple review corrected the
Windows formula independently: standard offset is -(Bias + StandardBias), while
DaylightBias remains excluded.
The adapter freezes the offset and hard-codes IsDaylightSavingTime to false. Fixing the
first without the second still reports "no DST" for a July date in New York. Both are repaired by
#2182; both are pinned separately.
CreateCustomTimeZone("X", TimeSpan::MinValue, …) is accepted. ConvertTimeToUtc then evaluates
-baseUtcOffset_ and the port throws "Negating the minimum value of a twos complement number is
invalid." — TimeSpan already guards the negation, so this is not undefined behaviour, but it
is a public door that produces a diagnostic about two's-complement arithmetic instead of rejecting
the argument. The ±14-hour validation closes the door at its source. Recorded because the audit
does not mention it and because it is the reason CCF-004 is not implicated here (§8).
Both CreateAdjustmentRule overloads accept the reversed range. Measurement also shows
dateStart with a time-of-day component, a daylightDelta of +15 hours, and a daylightDelta of
30 seconds are all accepted. Only the reversed range carried audit evidence at this checkpoint;
the other three went to #2186 rather than being repaired on a recollection. #2186 later answered
them from the reference and landed the required validation.
GetHashCode lowercases through std::tolower, whose result depends on the process LC_CTYPE.
Two zones whose ids differ only in the case of a non-ASCII byte can therefore hash equal or unequal
depending on a global the caller may change at any time. #2180 replaces both sides with one
ordinal ASCII fold so that equality and hash are computed by the same function.
sizeof(TimeZoneInfo) is 160 bytes on LP64 — measured, not derived
(build-probe/2185_layout.log): 4 × std::string = 128, TimeSpan = 24 (not the 8 an
estimate suggests; this port's TimeSpan is wider than a bare tick count), bool = 1, 7 bytes of
tail padding. A second bool fits that padding and is layout-neutral, confirmed at 160; a
std::vector<std::shared_ptr<AdjustmentRule>> (24 bytes) takes it to 184, and reordering the
members does not help — the best packing available is also 184. HasSameRules cannot return
false where .NET does without rule data to compare, so the complete repair needs that vector.
This was the measured layout cost of #2185's initial proposal. Later probes showed that the
proposal itself could not work: monthly sampling cannot distinguish New York from Havana, and a
single sampled year cannot represent rule eras. Faithful repair requires TZif rule structures,
which the 2026-08-19 user decision placed outside this practical subset. The 160 → 184 cost is
therefore recorded but not paid; SR-AUD-228 is an accepted permanent deviation, not a live gate.
Correction. Sections 5.7 and 11 of this document first carried an estimate of 144 → 168, written before the shape was compiled, and that estimate reached
docs/Migration-TimeZoneStandardOffset.md,README.mdand commit9210d8f's message. The measured numbers are 160 → 184. The substantive claim is unaffected — #2177–#2184 add no member andsizeofis 160 before and after, now pinned by astatic_assertinTimeZoneInfoTests.cpp— but the arithmetic behind the gate is restated here from measurement.9210d8fis already pushed and is not rewritten; this is the correction of record.
zoneFileExists() checks S_ISREG under /usr/share/zoneinfo/ and then hands the identifier
to setenv("TZ", …). glibc interprets a TZ value that is not a path as a POSIX rule string, so
zone.tab resolves as POSIX rule "zone", offset 0. Seven non-TZif regular files ship inside
/usr/share/zoneinfo and all seven are accepted as time zones today:
zone.tab zone1970.tab iso3166.tab tzdata.zi leapseconds leap-seconds.list -> accepted, +0m
A ':' prefix on the TZ value — the documented glibc way to force file interpretation — was
measured and does not help: bare and ':'-prefixed values give byte-identical results for all
nine ids tried (2176_probe3_design.log §A). The repair is therefore a TZif magic check at the
door, not a change to how TZ is set.
build-probe/2176_probe1_surface.log, one process, against the shipped library:
| Finding | Measurement |
|---|---|
| SR-AUD-223 | TZ=America/New_York: CurrentTimeZone().GetUtcOffset = −240 min for both January and July, IsDaylightSavingTime = 0 for both; system truth is −18000 s/isdst 0 and −14400 s/isdst 1 |
| SR-AUD-224 | TryFind("Mars/Olympus", out) → false, out still "UTC"; with a fresh out it stays null, so the stale value is the caller's |
| SR-AUD-225 | id="" accepted; +15h, −15h, 90-second and TimeSpan::MinValue offsets all accepted |
| SR-AUD-226 | CreateAdjustmentRule(2025-01-02, 2025-01-01, …) accepted on both overloads |
| SR-AUD-227 | Equals("Zone","zone") = 0 while GetHashCode values are equal |
| SR-AUD-228 | HasSameRules(America/New_York, America/Havana) = 1; .NET returns false |
| SR-AUD-229 | America/New_York records base=−240m std=EDT dst=EDT; standard is −300m/EST |
| Cause | Findings | Statement |
|---|---|---|
| TZ-A — one instant stands in for a year | SR-AUD-223, SR-AUD-229 | Zone metadata is read at time(nullptr) and frozen, so the answer depends on the month the process runs in |
| TZ-B — no input validation at a public factory | SR-AUD-225, SR-AUD-226, #2183 | Factories and the lookup door copy or accept whatever they are given |
| TZ-C — a result path that does not write its result | SR-AUD-224 | A false return leaves the out parameter untouched |
| TZ-D — equality and hash computed by different code | SR-AUD-227 | Two functions answer one question and disagree |
| TZ-E — an abbreviated model cannot express the contract it advertises | SR-AUD-228 | Offset + one bool cannot distinguish two rule sets |
| TZ-F — process-global state mutated without an unwind path | #2184 | setenv("TZ") is undone by straight-line code, not by a scope guard |
TZ-A, TZ-B and TZ-C are each fixed once and the fix covers every site of that cause. TZ-E is the one that needs storage the type does not have.
- CCF-004 (defined arithmetic) — closed at 8/8. The only signed-overflow-shaped door in this
namespace is §5.4's
-TimeSpan::MinValue, andTimeSpanalready guards it with a thrown diagnostic rather than wrapping. UBSan over the production bodies confirms it (§14). This is a validation defect, not an arithmetic-UB defect, so CCF-004 gains no member and none is claimed. - The NUL-truncation family (#2003 Uri, #2085 XmlWriter) gains a local occurrence in #2183:
FindSystemTimeZoneById("America/New_York\0junk")is accepted and stores the full 21-byte id. It is repaired here as part of the identifier door. No CCF is minted — three occurrences in three modules is exactly the promotion question #2131/#2109 already hold open, and this review does not pre-empt them. - CCF-019 / CCF-021 / CCF-022 — untouched. Nothing here hands out a borrowed raw pointer.
#2177 (TryFind out param) independent
#2178 (CreateCustomTimeZone guards) independent
#2179 (AdjustmentRule range) independent
#2180 (ordinal-case equality) independent
#2184 (TZ scope guard) ── prerequisite ──┐
#2183 (TZif + identifier door) ───────────────── ┼─> both edit the same locked region
#2181 (standard offset + names) ── needs #2184 ───┘ of FindSystemTimeZoneById
#2182 (legacy adapter per date) ── needs #2181 (correct standard/daylight names) and the
shared POSIX support header #2184 introduces
#2185 (SR-AUD-228) closed as a permanent deviation after design probes
#2186 (reference verification) completed after the reference became available
Implementation order: #2177 → #2178 → #2179 → #2180 → #2184 → #2183 → #2181 → #2182, then the two record-only tickets and the audit reconciliation.
All seven findings are medium; there are no high-severity findings in this namespace and none was created. The highest practical impact is SR-AUD-229 (141 of 499 zones wrong today, silently) and SR-AUD-223 (every DST-sensitive legacy query wrong for half the year, silently). Both are wrong-answer classes with no diagnostic; neither is a memory-safety class.
| Ticket | Class | Source break | ABI / layout / vtable | Behaviour change | Gate |
|---|---|---|---|---|---|
| #2177 | compatible | none | none | TryFind failure now nulls the out parameter |
none |
| #2178 | documented break | none | none | four previously-accepted inputs now throw | audit managed probe |
| #2179 | documented break | none | none | reversed date range now throws | audit managed probe |
| #2180 | documented break | none | none | case-variant ids now compare equal | audit managed probe |
| #2181 | documented break | none | none | BaseUtcOffset/StandardName/DaylightName become invariant |
contradicts own doc-comment |
| #2182 | compatible | none | none (file-local class) | legacy adapter becomes date-sensitive | none |
| #2183 | documented break | none | none | 7 data files and 4 malformed id shapes now throw | none needed — they were never zones |
| #2184 | compatible | none | none | an empty-but-set TZ is restored as empty |
none |
| #2185 | accepted deviation | none | none; the measured 160 → 184 proposal was not taken | HasSameRules remains one-directionally permissive |
closed: TZif rule parsing is out of scope |
| #2186 | documented compatibility repairs | none | none | conversions clamp; adjustment-rule validation and non-zone exception classification now match .NET | closed from reference evidence |
No public signature, virtual function, vtable slot, object layout, mangled symbol or
noexcept specification changes in #2177–#2184. Verified by static_assert in the tests and by
§13's layout pin.
BaseUtcOffsetbecomes invariant. For a DST zone queried during its daylight period,ConvertTime/ConvertTimeFromUtc/ConvertTimeToUtcshift by the daylight delta relative to today's answer — forAmerica/New_Yorkin July, one hour. This makes the conversion consistent with the documented "DST is not modelled" contract instead of silently depending on process start month. It is the deliberate consequence, it is documented indocs/Migration-TimeZoneStandardOffset.md, and it is pinned by tests.- The in-repository blast radius is nil in this container. The only consumers are
TimeProvider::getLocalTimeZoneProperty,FileSystemInfo's fourConvertTimeFromUtc/ConvertTimeToUtccalls and oneIOStreamTestsassertion, and all of them go throughTimeZoneInfo::Local()./etc/localtime → Etc/UTChere, soLocal()has zero offset and no DST and none of those results move. On a DST host,FileSystemInfo's local timestamps become standard-time-based, which is the same trade the property contract already states. TimeZone::CurrentTimeZone()becomes the one date-sensitive surface in the module. That is deliberate: the legacyTimeZonecontract is per-date, it only ever describes the process-local zone, and it can answer per date without storing anything.TimeZoneInfokeeps its documented fixed-offset model. The asymmetry is stated in both headers.- Ambiguous local times resolve to standard in the legacy adapter. Raw
mktime()withtm_isdst = -1was measured to depend on unrelated preceding calls; #2186 established .NET's standard reading and the adapter now asks both interpretations explicitly. - Invalid local times normalise forward (02:30 on a spring-forward date answers with the post-transition offset) for the same reason.
| Platform | Effect |
|---|---|
| Linux/POSIX | all of it; this is the tested baseline |
| Windows | #2418 repairs the Bias + StandardBias standard-offset formula, keeps standard and daylight names distinct, and makes the legacy TimeZone adapter select the per-year system rule for local-wall-clock and UTC-instant questions. The branch was reviewed for MSVC portability, but the tracked CI is Ubuntu-only and does not compile or execute it. |
| Emscripten | The zero-offset model remains, but Local() is now a distinct Local-identity object rather than the canonical Utc singleton, so Kind propagation remains truthful. Non-Local/non-UTC database lookup is still unsupported. |
| macOS | uses the POSIX branch. /usr/share/zoneinfo is present on macOS, so the TZif check and the month scan behave as on Linux. Not executed here — the repository's tracked CI is Ubuntu-only |
No POSIX header enters any public .hpp. The new shared helper lives in
modules/time-zone/src/System/TimeZonePosixSupport.hpp, a private header under src/, which is
an established pattern in this repository (5 such headers exist in net-http-headers, xml and
net-sockets).
Real installed zones chosen for distinct properties, all verified present
(2176_probe1_surface.log §0):
| Zone | Property it exercises |
|---|---|
Etc/UTC, UTC |
no transitions; UTC is a symlink to Etc/UTC |
Etc/GMT+5, Etc/GMT-14 |
fixed offsets, including the +14 h extreme |
America/New_York |
northern DST; ambiguous and invalid local times |
America/Havana |
same offset and DST flag as New York, different rules (SR-AUD-228) |
Europe/Prague |
northern DST, positive offset |
Europe/Dublin |
standard offset zero, DST positive |
Africa/Casablanca |
DST active in both January and July — the scan's fallback path |
Asia/Kolkata (+05:30), Asia/Kathmandu (+05:45), Asia/Tehran (+03:30) |
non-whole-hour offsets, no DST |
Australia/Lord_Howe |
30-minute DST delta |
Pacific/Chatham |
+12:45 standard, +13:45 daylight |
Australia/Sydney |
southern hemisphere — correct today, wrong in January |
America/Phoenix |
never observes DST |
EST5EDT |
an id that is also a valid POSIX rule string |
zone.tab, iso3166.tab, tzdata.zi, leapseconds, leap-seconds.list, zone1970.tab |
the six shipped non-TZif regular files — malformed-input fixtures that need no file to be written |
Transition-boundary matrix, applied to America/New_York for 2025 (spring forward 2025-03-09
02:00, fall back 2025-11-02 02:00) in both the legacy adapter and TimeZoneInfo: 01:59 / 02:00 /
02:30 (gap) / 03:00 on the spring date; 00:59 / 01:00 / 01:30 (repeated) / 03:00 on the autumn
date; an ordinary standard date; an ordinary daylight date; both year boundaries. Directions
covered: local → UTC, UTC → local, and zone A → zone B (America/New_York → Asia/Kolkata).
Identifier matrix: "", " ", "..", "../../etc/passwd", "/etc/passwd",
"America//New_York", "America/./New_York", "./America/New_York", "america/new_york",
"America" (a directory), "America/New_York/", an embedded NUL, "UTC", "Local".
Range matrix: DateTime::MinValue and DateTime::MaxValue against ±14-hour zones in all four
conversion directions; TimeSpan::MinValue/MaxValue at the factory.
Replacing the TZif-parser matrix. This module parses no TZif, so the brief's parser matrix (truncated header, invalid counts, bad type index, missing abbreviation terminator, leap records) has no code here to test — libc owns it. What is testable, and is tested, is the door: does a file that is not a TZif file get accepted as a zone? Six shipped non-TZif files answer that question without writing a byte, and #2183 is the repair.
| Sanitizer | Applies? | Why |
|---|---|---|
| UBSan | yes | offset arithmetic: TimeSpan negation, FromSeconds(double) conversions, the tick multiplications in the month scan, intcs/long mixing in the POSIX helpers |
| ASan | yes | tm_zone is a pointer into libc-owned storage that tzset() may invalidate; the month scan reads it repeatedly across setenv/tzset cycles. Also covers the stat/fopen paths #2183 adds |
| LSan | yes (with ASan) | the failure paths of #2183 must not leak the FILE* or a partially built zone |
| TSan | yes, and it is discriminating here | the module holds real shared mutable state: Core.Base's System::detail::processTimeZoneMutex(), the Local() function-local static, and process-global TZ/tzset(). Concurrent Local() and FindSystemTimeZoneById() is exactly the race the shared mutex prevents |
TSan is run because there is shared mutable state to exercise, not as a formality — and a clean TSan run is reported as "no report on this workload", never as "the module is thread-safe".
| # | P | Size | Title |
|---|---|---|---|
| #2176 | P2 | M | review modules/time-zone and produce this plan |
| #2177 | P2 | XS | SR-AUD-224 — TryFindSystemTimeZoneById must clear its out parameter on failure |
| #2178 | P2 | S | SR-AUD-225 — CreateCustomTimeZone must validate the id and the offset |
| #2179 | P2 | S | SR-AUD-226 — AdjustmentRule must reject an end date before its start date |
| #2180 | P2 | S | SR-AUD-227 — zone equality must be ordinal case-insensitive, like its hash |
| #2181 | P2 | M | SR-AUD-229 — BaseUtcOffset, StandardName and DaylightName must be invariant |
| #2182 | P2 | M | SR-AUD-223 — the legacy CurrentTimeZone adapter must answer per date |
| #2183 | P2 | M | a non-TZif file and a malformed identifier are accepted as time zones |
| #2184 | P3 | S | the TZ save/restore window is not exception-safe and drops an empty-but-set TZ |
| #2185 | P2 | — | CLOSED — SR-AUD-228 accepted as a permanent one-directional deviation; faithful TZif rule parsing is out of scope |
| #2186 | P3 | — | CLOSED — five reference-parity questions answered; three repairs and two already-correct outcomes |
| Question | Reference answer and disposition |
|---|---|
Do ConvertTimeFromUtc/ConvertTimeToUtc clamp at DateTime::MinValue/MaxValue? |
Yes. The shared safe constructor clamps once at the final result; repaired and tested. |
Which adjacent AdjustmentRule.CreateAdjustmentRule shapes are invalid? |
Non-date-only boundaries, deltas outside -23..+14 hours, and deltas not expressed in whole minutes are rejected. The original ±14/sub-minute recollection was corrected from source. |
| What are the exact resource strings for those argument diagnostics? | Transcribed from the reference and pinned. |
Does TryFindSystemTimeZoneById return false for every failure, and what does the throwing door do with non-zone data? |
The Try door returns false for every failure; the throwing door uses InvalidTimeZoneException for existing non-TZif data and TimeZoneNotFoundException for an absent id. Repaired and tested. |
| Which offset wins for an ambiguous local wall clock? | The standard-time interpretation wins. The legacy adapter already chose that result; the reference confirmed it. |
docs/Migration-TimeZoneClampAndValidation.md records the source locations, mutations, and exact
regressions. Nothing in this section remains deferred.
DisplayNameis the raw id for system zones ("Europe/Prague", not"(UTC+01:00) Central European Standard Time"). Producing .NET's text needs CLDR display data this repository does not carry. Out of scope; unchanged.- Serialization (
ToSerializedString/FromSerializedString) — absent, and stays absent perCLAUDE.md's permanent-deviation list. ClearCachedData()invalidation andGetSystemTimeZones()enumerating the database remain documented practical-subset feature gaps. They are pinned beside #2186's regressions but were not among its five reference questions and are not represented as deferred verification.- Modelling DST inside
TimeZoneInfo(GetUtcOffset,IsDaylightSavingTime,IsAmbiguousTime,IsInvalidTime,GetAmbiguousTimeOffsets,GetAdjustmentRules) — the header's documented limitation. #2185 established that faithful parity requires TZif rule parsing, which is an accepted permanent deviation for this practical subset. InvalidTimeZoneExceptionis now thrown for an existing non-TZif data file; #2186 settled the earlier #2183 uncertainty from the reference. Missing identifiers still throwTimeZoneNotFoundException, and the Try door returns false for both.- Windows and Emscripten runtime behaviour — #2418 reviewed and repaired these conditional branches and made their tests compile-condition-aware, but the repository's tracked CI is Ubuntu-only: there is no Windows/Emscripten cross-build or runtime result to claim here.
HasSameRules compares baseUtcOffset_ and supportsDst_ and nothing else. With #2181 landed,
America/New_York and America/Havana both report a standard offset of −05:00 and
SupportsDaylightSavingTime true, so the method returns true; .NET returns false, because their
transition dates differ. Fixing #2181 did not fix this and could not: the two zones agree on
everything this type stores.
Note the direction of the failure. This port can never return false where .NET returns true —
it can only be too permissive. That bounds the risk of leaving it as it is, and it is why the
finding is medium rather than high.
Give TimeZoneInfo a std::vector<std::shared_ptr<AdjustmentRule>> rules_, populate it in
FindSystemTimeZoneById from the year scan #2181 already performs (each scan already observes the
standard offset, the daylight offset, whether a transition occurs and in which months), return it
from GetAdjustmentRules(), and make HasSameRules compare baseUtcOffset_, supportsDst_ and
the rule vector element-wise via the AdjustmentRule::Equals that already exists.
Two later probes invalidated that proposal. The existing monthly scan produces byte-identical
2025 samples for New York and Havana even though their transition hours differ. A finer one-year
transition scan still cannot model the several historical rule eras that .NET returns as separate
AdjustmentRule values. Faithful content requires the TZif POSIX footer or its full transition
table; libc exposes resolved instants, not the recurrence rules. Sampling at any granularity would
therefore invent .NET-named adjustment rules rather than implement them.
Three alternatives were considered and rejected:
| Alternative | Why not |
|---|---|
Compare id_ as well |
Wrong for the case .NET gets right: two differently named zones with identical rules must compare true. Comparing ids would turn a permissive answer into a wrong one. |
| Compare the observed UTC-offset function over a sampled window | Invents an algorithm .NET does not use, and its answer depends on the sampling window. The review declines to invent. |
Derive rules on demand inside HasSameRules without storing them |
Avoids the layout change but makes an equality-shaped method perform twelve setenv/tzset cycles per zone per call, under the process-global timezone lock. |
build-probe/2185_layout.log, three shapes compiled side by side against the real
AdjustmentRule:
TimeZoneInfo (real) sizeof 160 align 8 <- shipped, unchanged by #2177-#2184
model: as shipped sizeof 160 align 8
model: + one bool sizeof 160 align 8 <- the tail padding absorbs it
model: + rule vector sizeof 184 align 8 <- the repair
model: + rule vector, reordered sizeof 184 align 8 <- no ordering avoids it
std::string is 32, TimeSpan is 24, bool is 1: 128 + 24 + 1 = 153, padded to 160, leaving
seven bytes — enough for another bool, not for a 24-byte vector.
TimeZoneInfo has no virtual functions, so no vtable is involved and no mangled name changes; the
break is purely that a translation unit compiled against the old header believes the object is 160
bytes and one compiled against the new header believes 184. Instances are handed out through
std::shared_ptr from the factories and as const& from Utc()/Local(), so a consumer is
unlikely to embed one by value — but the type is copyable and nothing prevents it, which is exactly
the situation README.md's 2026-07-29 BitArray::Enumerator entry records as linking with zero
diagnostics and then giving silently wrong answers.
The original approval question was:
"
System::TimeZoneInfomay grow from 160 to 184 bytes (onestd::vector<std::shared_ptr<AdjustmentRule>>) so thatHasSameRulesandGetAdjustmentRulescan distinguish two zones that share a base offset and a daylight flag but not their transition rules, requiring every consumer to be rebuilt."
The 2026-08-19 decision is that TZif rule parsing is out of scope. The finding is therefore an
accepted-deviation: this implementation can be too permissive but never too strict. The layout
change was not made. The current contract is held by four declaration tests
(PIN_HasSameRules_CannotDistinguishNewYorkFromHavana,
PIN_HasSameRules_OnlyDistinguishesOffsetAndTheDaylightFlag,
PIN_GetAdjustmentRules_IsEmptyForEverySystemZone, and a static_assert on sizeof), so a future
scope change cannot silently erase the decision.
modules/time-zone is closed for its declared scope because:
- all seven findings carry exactly one disposition and none has disappeared;
- #2177–#2184 are implemented, each with permanent tests, and each committed and pushed separately;
- SR-AUD-223/224/225/226/227/229 read
remediatedinaudit/AUDIT_FINDINGS_INDEX.md; - SR-AUD-228 reads
accepted-deviation, with #2185 recording why sampled rules are not faithful and why TZif rule parsing is outside scope; - every behaviour change is pinned by a test that fails if it is reverted, proven by mutation;
- the deliberate breaks are documented in
docs/Migration-TimeZoneStandardOffset.mdandREADME.md; - the current full 38-executable gate passes without failures or skips;
- UBSan, ASan+LSan and TSan run over the production bodies with zero reports.
SR-AUD-228 remains a deliberate compatibility limit, not actionable unfinished work.