Component: primitives/team_mesh.py — _parse_iso() (L103), as consumed by the envelope expiry gate in verify_envelope() (~L216-223), the poll-loop entry receive() (~L302). Same defect in primitives/team_approval.py (L190, L371).
The defect._parse_iso converts a UTC (…Z) timestamp with LOCAL-time mktime:
def _parse_iso(s):
try:
return time.mktime(time.strptime(s, "%Y-%m-%dT%H:%M:%SZ")) - time.timezone
except Exception:
return None
time.strptime returns a struct with tm_isdst = -1, so time.mktime applies the DST offset in effect for THAT date, while the correction subtracts time.timezone (the STANDARD-time offset). On a DST-observing host during summer the two differ by exactly 3600s, so every parsed timestamp lands one hour early. (Verified: America/New_York summer → mktime-based − calendar.timegm = −3600s; winter → 0.)
The station already knows this is wrong and fixed it — everywhere except the mesh. primitives/entitlement.py:70-76 documents the exact rule and uses calendar.timegm:
# The timestamps are UTC ("…Z"). calendar.timegm treats the parsed struct_time
# as UTC (the inverse of gmtime); time.mktime would (wrongly) treat it as LOCAL …
return float(calendar.timegm(time.strptime(s, "%Y-%m-%dT%H:%M:%SZ")))
module_entitlement.py:77 matches. Only team_mesh.py:103 and team_approval.py:190/371 still carry the mktime − time.timezone form — the migration to timegm skipped the mesh/approval files.
Impact — a total team-mesh outage on a DST host in summer. _parse_iso feeds the expiry gate:
exp = _parse_iso(env.get("expires_at"))
if exp is not None and exp + CLOCK_SKEW_S < now: # ~L216/L219
return "expired"
With DEFAULT_TTL_S = 900 and CLOCK_SKEW_S = 90, a freshly-minted envelope has parsed exp = now + 900 − 3600 = now − 2700; then exp + 90 < now is true → EVERY inbound envelope is rejected as "expired". Everything that rides verify_envelope — approvals, capability shares, team jobs, policy sync — stops. The mint side is correct (uses gmtime), so the sender believes it sent a valid 15-minute envelope; the receiver rejects all of them.
Reproduction.
- On a DST-observing host (e.g. TZ=America/New_York) during summer, or simulate:
TZ=America/New_York python3and comparetime.mktime(time.strptime("<Z ts>", "%Y-%m-%dT%H:%M:%SZ")) - time.timezoneagainstcalendar.timegm(time.strptime("<Z ts>", …))— the difference is −3600 in summer, 0 in winter. - Mint a normal envelope (TTL 900s) and feed it to
verify_envelopeon that host:_parse_iso(expires_at)returnsnow − 2700, the expiry gate returns"expired", the envelope is dropped. - Every subsequent inbound mesh envelope is dropped identically → approvals/shares/jobs/policy-sync all fail until DST ends.
(Invisible on a host with no DST — e.g. UTC−3 Brazil since 2019 — because tm_isdst guesses 0 and the arithmetic is exact there. That is why it survives local testing.)
Expected (correct) behavior: parse the UTC timestamp with calendar.timegm (the same fix the licensing files already carry), so the expiry gate uses the true instant regardless of host DST.
Scope. Availability only — the shift is always earlier, so envelopes expire SOONER, never accepted later; no bypass of the expiry check, no replay hole (_remember pruning uses the same _parse_iso, so both shift together). Precondition: the receiving station runs in a DST-observing timezone during its DST period.
Fix. import calendar and replace _parse_iso's body with return float(calendar.timegm(time.strptime(s, "%Y-%m-%dT%H:%M:%SZ"))) — and apply the identical change to team_approval.py:190 and :371, finishing the migration the licensing files already made.
Classification: CWE-697