Skip to content

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

@Scared-Heart

Description

@Scared-Heart

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.crpc_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.

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