Skip to content

Year 2038: server-link timestamps parsed with atoi() truncate at INT_MAX (protocol-neutral, fixable now) #119

Description

@MrIron-no

Summary

Timestamps arriving on server links are parsed with atoi(), which
returns int. On every platform ircu targets that is 32 bits, so the
parse saturates at 2,147,483,647 — 2038-01-19 03:14:07 UTC. This is
a genuine Year 2038 bug, and it bites even on a 64-bit time_t
system
, because the truncation happens inside atoi() before the
value is ever widened into the time_t it is assigned to:

time_t creation = atoi(parv[2]);   /* truncated at INT_MAX, then widened */

Good news: the fix is input-only and protocol-neutral, so it can be
done today, independent of the P11 series. It is not a wire-format
change.

Why it needs no protocol change

  • The output side is already 64-bit clean: ircd_snprintf's %Tu
    reads a full time_t via va_arg (ircd_snprintf.c:1821, :1844),
    so timestamps already go onto the wire as complete decimal strings. A
    post-2038 value like 2147483648 transmits fine today.
  • The wire format is decimal ASCII, which has no 32-bit ceiling. A fixed
    receiver reading 2147483648 with a wide parse just works, regardless
    of the sender's age. There is no flag day.

There is already precedent for the correct idiom in the tree —
m_sline.c:128:

lastmod = (time_t)strtoll(parv[2], NULL, 10);

and s_user.c:1516 even carries the comment "pity we don't have
strtoul everywhere." This issue is about finishing that conversion.

Fix

Replace atoi() with (time_t)strtoll(str, NULL, 10) at every site
that parses an epoch (or a relative offset that becomes one), ideally
behind a small helper — e.g. time_t atotime(const char *) next to the
other string helpers — so the intent is legible and future audits are
grep-able.

Timestamp sites to widen (~27 across 13 files)

File / site Value
m_server.c:565,566,729,730 server link start_timestamp, timestamp
m_squit.c:90 SQUIT server timestamp
m_nick.c:310,316 nick lastnick
s_user.c:539,620 cli_lastnick
m_create.c:124, m_join.c:343 channel creation TS
m_burst.c:228, m_destruct.c:102 channel TS (burst / destruct)
channel.c:3523 recv_ts — the MODE channel TS (see #114)
m_topic.c:203,207 topic time / creation time
m_invite.c:276 invite TS
m_settime.c:120,220 SETTIME target time
m_gline.c:223,294,299,301,306 G-line expire / lastmod / lifetime
m_jupe.c:150,151,213,218 jupe lastmod (absolute) and expire_off
m_rping.c:150 link lag sample — also widen the local int timestamp

G-line expire and jupe expire_off arrive as relative offsets and are
made absolute (abs_expire adds CurrentTime); they still overflow if
the offset alone exceeds INT_MAX, and lastmod/lifetime are
absolute, so all of them should be widened.

A second, related 32-bit truncation (not strictly 2038)

acc_id is declared uint64_t (struct.h:91) but parsed with atoi()
at s_user.c:1262 and m_account.c:135, so account IDs above 2^31
truncate today. Parse with strtoull. (acc_flags at s_user.c:1270 /
m_account.c:138 is a flag word — widen for consistency, low risk.)

Explicitly out of scope — leave as atoi()

About 40 remaining atoi() calls parse non-time values and have no 2038
exposure: TCP ports (listener.c, m_connect.c, m_uping.c,
uping.c), hop counts, protocol numbers (m_server.c:94-96,
ircd.c:785), op-levels (channel.c:3159,3181), CAP version
(m_cap.c:274), WHOWAS max, umode mask (s_user.c:1517),
debug/oper-class levels, config ints (ircd_netconf.c), and the
millisecond RTT deltas (m_pong, m_ping, m_asll, opercmds — these
are differences, not epochs). No change needed.

What belongs to the P11 roll-out

Nothing about 2038 requires P11, but two items are worth pinning to it:

  1. Parse new P11 timestamp fields wide from day one — the mandatory
    MODE timestamp (Make the channel timestamp mandatory on MODE (M) in the next protocol revision — the current optional-TS heuristic can be tricked into setting creationtime to a garbage value #114) and the burst ban setter/timestamp (Burst ban setter and timestamp in P11 — ban metadata is currently discarded on every relink #116)
    introduce new epoch fields; write their parsers with the wide path so
    they are not re-audited later.
  2. Add a regression test. There is currently no coverage that a
    post-2038 timestamp survives. Feed a NICK/CREATE/MODE/burst
    line carrying a TS like 2200000000 (year 2039) and assert it
    round-trips through parse and re-emit unchanged. That test fails on
    today's atoi() code and locks in the fix; it should assert on the
    receiving (leaf) side, not just locally.

Build caveat

The parse fix assumes a 64-bit time_t, which holds on any 64-bit
build. On a 32-bit target with glibc >= 2.34 you additionally need
-D_TIME_BITS=64 (with -D_FILE_OFFSET_BITS=64), or time_t itself is
32-bit and the wide parse overflows one step later. A configure check
that warns on a 32-bit time_t build would be a reasonable follow-up,
separate from and lower priority than the atoi() work.

Suggested sequencing

Happy to open the PR for the conversion if there's agreement.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions