Skip to content

session: clamp uloop timeout to avoid int overflow#30

Open
Scared-Heart wants to merge 1 commit intoopenwrt:masterfrom
Scared-Heart:fix/session-timeout-int-overflow
Open

session: clamp uloop timeout to avoid int overflow#30
Scared-Heart wants to merge 1 commit intoopenwrt:masterfrom
Scared-Heart:fix/session-timeout-int-overflow

Conversation

@Scared-Heart
Copy link
Copy Markdown

@Scared-Heart Scared-Heart commented Apr 16, 2026

Fixes #29

rpc_touch_session() computes ses->timeout * 1000 as int*int, which overflows INT_MAX once ses->timeout exceeds 2147483 seconds (~24.85 days). The wrapped-around negative value passed to uloop_timeout_set() causes libubox to fire the session timeout callback on the very next uloop iteration, destroying the just-created session before the caller can use it.

In practice this makes ubus call session login with e.g. timeout:2592000 (30 days) return a valid-looking session id whose reported expires is a large negative number, and any subsequent session.set / session.get call on that SID returns "Not found". LuCI's ucode dispatcher passes the value of luci.sauth.sessiontime straight through as the login timeout, so users who follow common advice to bump sessiontime to 30 days get silently locked out of LuCI (uhttpd logs accepted login, response is 403 with no Set-Cookie) while SSH keeps working with the same credentials. At least one affected user resorted to factory-resetting multiple APs before recovering, see https://forum.openwrt.org/t/241892 .

The same overflow lurks in rpc_session_from_blob() when thawing a persisted session, where blobmsg_get_u64(EXPIRES) * 1000 is passed to uloop_timeout_set()'s int msecs parameter.

Introduce a small helper that converts a seconds value to milliseconds with clamping to INT_MAX (and negative-input guard), and use it at both call sites. The cap still allows uloop timeouts of ~24.85 days, which is longer than any realistic administrative session.

@Scared-Heart
Copy link
Copy Markdown
Author

Summary

rpc_touch_session() computes ses->timeout * 1000 as int * int. When ses->timeout exceeds INT_MAX / 1000 ≈ 2,147,483 seconds (~24.85 days), the product wraps to a large negative value. uloop_timeout_set() treats that as "already expired" and fires the session timeout callback on the very next uloop iteration — destroying the session before the caller can use it.

The same overflow exists in rpc_session_from_blob() when thawing a persisted session, where blobmsg_get_u64(EXPIRES) * 1000 is passed to uloop_timeout_set()'s int msecs.

User-visible impact

Any ubus session login call with a large timeout returns a SID whose subsequent session.set / session.get calls fail with Not found.

LuCI's ucode dispatcher (luci-base's dispatcher.uc:session_setup) forwards the value of luci.sauth.sessiontime directly as the login timeout. Users who follow popular guides and set sessiontime=2592000 (30 days) get silently locked out of LuCI: uhttpd logs luci: accepted login on / for root, but the response is 403 with X-LuCI-Login-Required: yes and no Set-Cookie. SSH with the same credentials continues to work, making the cause very non-obvious.

At least one affected user had to factory-reset several APs before recovering — see forum thread: https://forum.openwrt.org/t/how-to-set-session-timeout-without-having-to-reset-and-reconfigure/241892.

Reproduction

# Safe:
$ ubus call session login '{"username":"root","password":"<pw>","timeout":300}'
{ "timeout": 300, "expires": 300, ... }

# Triggers overflow (30 days):
$ ubus call session login '{"username":"root","password":"<pw>","timeout":2592000}'
{ "timeout": 2592000, "expires": -1702967, ... }

$ ubus call session set '{"ubus_rpc_session":"<sid>","values":{"x":"y"}}'
Command failed: ... (Not found)

-1702967 is exactly (2592000 * 1000 − 2^32) / 1000, confirming 32-bit signed overflow of the millisecond product.

Fix

Introduce a small helper rpc_session_timeout_ms() that converts a seconds value (accepted as int64_t for the thaw path) to milliseconds, clamping to INT_MAX and guarding against negative input. Use it at both uloop_timeout_set call sites.

The cap still permits ~24.85 days of idle timeout, longer than any realistic administrative session. Values larger than that are silently capped rather than rejected, preserving current API behavior for existing callers.

Test plan

  • Unit-tested the helper in isolation with -Wall -Wextra -Werror -std=c11:
    • 300 → 300000 ms
    • 86400 → 86400000 ms
    • 2147483 → 2147483000 ms (safe edge, no clamp)
    • 2147484 → INT_MAX (just over, clamped)
    • 2592000 → INT_MAX (the bug reproducer, clamped)
    • 0 → 0 ms, -1 → 0 ms (negative-input guard)
  • Observed on OpenWrt 23.05.3 that setting luci.sauth.sessiontime ≤ 2073600 restores LuCI login without this patch, confirming the overflow boundary matches the analysis.
  • Post-patch full validation requires the OpenWrt build env; I have not run a cross-compiled smoke test yet.

rpc_touch_session() computes `ses->timeout * 1000` as int*int, which
overflows INT_MAX once ses->timeout exceeds 2147483 seconds (~24.85
days). The wrapped-around negative value passed to uloop_timeout_set()
causes libubox to fire the session timeout callback on the very next
uloop iteration, destroying the just-created session before the caller
can use it.

In practice this makes `ubus call session login` with e.g.
`timeout:2592000` (30 days) return a valid-looking session id whose
reported `expires` is a large negative number, and any subsequent
session.set / session.get call on that SID returns "Not found". LuCI's
ucode dispatcher passes the value of `luci.sauth.sessiontime` straight
through as the login timeout, so users who follow common advice to bump
sessiontime to 30 days get silently locked out of LuCI (uhttpd logs
`accepted login`, response is 403 with no Set-Cookie) while SSH keeps
working with the same credentials. At least one affected user resorted
to factory-resetting multiple APs before recovering, see
https://forum.openwrt.org/t/241892 .

The same overflow lurks in rpc_session_from_blob() when thawing a
persisted session, where `blobmsg_get_u64(EXPIRES) * 1000` is passed to
uloop_timeout_set()'s int `msecs` parameter.

Introduce a small helper that converts a seconds value to milliseconds
with clamping to INT_MAX (and negative-input guard), and use it at both
call sites. The cap still allows uloop timeouts of ~24.85 days, which
is longer than any realistic administrative session.

Signed-off-by: Breeze <chanlikessummer@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Scared-Heart Scared-Heart force-pushed the fix/session-timeout-int-overflow branch from 380fbc4 to d005c88 Compare April 16, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

session: timeout > ~24.8 days causes immediate session expiry due to int32 overflow in rpc_touch_session()

1 participant