Describe the bug
When session.login (or session.create) is called with a timeout value greater than INT_MAX / 1000 ≈ 2,147,483 seconds
(~24.85 days), the newly created session is destroyed immediately. All subsequent session.set / session.get /
session.access calls on that SID return Not found.
From the outside the session appears to be born already expired — its reported expires is a large negative value, as if the
session was created several weeks in the past.
The user-facing impact: LuCI web login silently fails with HTTP/1.1 403 Forbidden + X-LuCI-Login-Required: yes, while
uhttpd happily logs luci: accepted login on / for root. SSH with the same credentials keeps working, so administrators see a
very misleading "wrong username or password" screen in the browser even though credentials validated server-side.
This is routinely triggered by users following community advice to set luci.sauth.sessiontime to 2592000 (30 days) — a value
that just happens to straddle the overflow boundary.
Steps to reproduce
# Safe timeout — works:
$ ubus call session login '{"username":"root","password":"<correct>"}'
{ "ubus_rpc_session": "...", "timeout": 300, "expires": 300, ... }
# 30-day timeout — session born expired:
$ ubus call session login '{"username":"root","password":"<correct>","timeout":2592000}'
{ "ubus_rpc_session": "abc...", "timeout": 2592000, "expires": -1702967, ... }
# Any follow-up call on the returned SID:
$ ubus call session set '{"ubus_rpc_session":"abc...","values":{"token":"x"}}'
Command failed: ... (Not found)
The observed expires: -1702967 is exactly (2592000 * 1000 − 2^32) / 1000.
Via HTTP, LuCI's ucode dispatcher (dispatcher.uc:session_setup) passes luci.sauth.sessiontime straight through as the
timeout argument, so any sessiontime > 2147483 reproduces the lockout on the next login.
Root cause
session.c — rpc_touch_session():
static void rpc_touch_session(struct rpc_session *ses)
{
if (ses->timeout > 0)
uloop_timeout_set(&ses->t, ses->timeout * 1000);
}
ses->timeout is an int.
ses->timeout * 1000 is evaluated as int * int → int.
- For
ses->timeout > 2,147,483 the product overflows INT_MAX and wraps to a large negative value.
uloop_timeout_set() takes an int msecs; a negative value schedules the callback in the past, so rpc_session_timeout()
fires on the very next uloop iteration and calls rpc_session_destroy().
Additionally, login_policy[RPC_L_TIMEOUT] and new_policy[RPC_SN_TIMEOUT] are declared BLOBMSG_TYPE_INT32 with no
upper-bound validation, so any caller-supplied value up to UINT32_MAX is silently accepted and produces a broken session.
The expires field is reported via uloop_timeout_remaining64(&ses->t) / 1000, which faithfully reflects the past-due
wall-clock deadline — hence the very negative value visible in session.list output.
Impact
- Any user increasing
luci.sauth.sessiontime to values ≥ ~24.85 days gets locked out of LuCI after their next successful
credential check.
- The failure mode is extremely misleading:
uhttpd logs accepted login, but the browser sees 403 + "wrong credentials".
- SSH still works, so the config can be repaired — but the root cause is non-obvious enough that at least one affected user
resorted to factory-resetting three APs: [forum.openwrt.org/t/241892](https://forum.openwrt.org/t/how-to-set-session-timeout-with
out-having-to-reset-and-reconfigure/241892).
Environment
- OpenWrt 23.05.3, r23809-234f1a2efa, x86/64
- rpcd shipped with 23.05.3
- Also reproducible on current
master per code inspection (no relevant change in rpc_touch_session since the code was
introduced)
- LuCI
luci-base git-25.222.75657-7ce34fe (ucode dispatcher), but the bug is independent of LuCI — any ubus client can hit it.
Suggested fix
Clamp on input and defend on use. Diff sketch:
/* session.c */
static void rpc_touch_session(struct rpc_session *ses)
{
if (ses->timeout > 0) {
int msecs = (ses->timeout > INT_MAX / 1000)
? INT_MAX
: ses->timeout * 1000;
uloop_timeout_set(&ses->t, msecs);
}
}
static int rpc_handle_login(...)
{
...
if (tb[RPC_L_TIMEOUT]) {
uint32_t t = blobmsg_get_u32(tb[RPC_L_TIMEOUT]);
timeout = (t > (uint32_t)(INT_MAX / 1000))
? INT_MAX / 1000
: (int)t;
}
...
}
/* Same clamp in rpc_handle_create(). */
Either change alone fixes the lockout; both together give defense in depth. If you prefer rejection over clamping at the API
boundary, returning UBUS_STATUS_INVALID_ARGUMENT for out-of-range timeouts would be equally fine and arguably clearer for
clients.
Workaround (for affected users)
Keep luci.sauth.sessiontime ≤ 2073600 (24 days) until fixed:
uci set luci.sauth.sessiontime=604800 # 7 days, safe
uci commit luci
No service restart required — LuCI re-reads UCI per request.
Describe the bug
When
session.login(orsession.create) is called with atimeoutvalue greater thanINT_MAX / 1000 ≈ 2,147,483seconds(~24.85 days), the newly created session is destroyed immediately. All subsequent
session.set/session.get/session.accesscalls on that SID returnNot found.From the outside the session appears to be born already expired — its reported
expiresis a large negative value, as if thesession was created several weeks in the past.
The user-facing impact: LuCI web login silently fails with
HTTP/1.1 403 Forbidden+X-LuCI-Login-Required: yes, whileuhttpdhappily logsluci: accepted login on / for root. SSH with the same credentials keeps working, so administrators see avery misleading "wrong username or password" screen in the browser even though credentials validated server-side.
This is routinely triggered by users following community advice to set
luci.sauth.sessiontimeto2592000(30 days) — a valuethat just happens to straddle the overflow boundary.
Steps to reproduce
The observed
expires: -1702967is exactly(2592000 * 1000 − 2^32) / 1000.Via HTTP, LuCI's ucode dispatcher (
dispatcher.uc:session_setup) passesluci.sauth.sessiontimestraight through as thetimeoutargument, so anysessiontime > 2147483reproduces the lockout on the next login.Root cause
session.c—rpc_touch_session():ses->timeoutis anint.ses->timeout * 1000is evaluated asint * int → int.ses->timeout > 2,147,483the product overflowsINT_MAXand wraps to a large negative value.uloop_timeout_set()takes anint msecs; a negative value schedules the callback in the past, sorpc_session_timeout()fires on the very next uloop iteration and calls
rpc_session_destroy().Additionally,
login_policy[RPC_L_TIMEOUT]andnew_policy[RPC_SN_TIMEOUT]are declaredBLOBMSG_TYPE_INT32with noupper-bound validation, so any caller-supplied value up to
UINT32_MAXis silently accepted and produces a broken session.The
expiresfield is reported viauloop_timeout_remaining64(&ses->t) / 1000, which faithfully reflects the past-duewall-clock deadline — hence the very negative value visible in
session.listoutput.Impact
luci.sauth.sessiontimeto values ≥ ~24.85 days gets locked out of LuCI after their next successfulcredential check.
uhttpdlogsaccepted login, but the browser sees 403 + "wrong credentials".resorted to factory-resetting three APs: [forum.openwrt.org/t/241892](https://forum.openwrt.org/t/how-to-set-session-timeout-with
out-having-to-reset-and-reconfigure/241892).
Environment
masterper code inspection (no relevant change inrpc_touch_sessionsince the code wasintroduced)
luci-basegit-25.222.75657-7ce34fe (ucode dispatcher), but the bug is independent of LuCI — any ubus client can hit it.Suggested fix
Clamp on input and defend on use. Diff sketch:
Either change alone fixes the lockout; both together give defense in depth. If you prefer rejection over clamping at the API
boundary, returning
UBUS_STATUS_INVALID_ARGUMENTfor out-of-range timeouts would be equally fine and arguably clearer forclients.
Workaround (for affected users)
Keep
luci.sauth.sessiontime ≤ 2073600(24 days) until fixed:No service restart required — LuCI re-reads UCI per request.