View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0006639 | unreal | json-rpc | public | 2026-07-26 11:45 | 2026-08-17 15:53 |
| Reporter | aramosf | Assigned To | syzop | ||
| Priority | high | Severity | major | Reproducibility | always |
| Status | resolved | Resolution | fixed | ||
| Product Version | 6.2.6 | ||||
| Fixed in Version | 6.2.7 | ||||
| Summary | 0006639: UnrealIRCd JSON-RPC Timer Use-After-Free | ||||
| Description | | **Title** | Use-after-free in UnrealIRCd JSON-RPC timer event loop (`rpc_do_timers`) | | **Author** | A. Ramos <[email protected] | | **CVE** | Not assigned (pre-disclosure) | | **CWE** | **CWE-416** — Use After Free | | **CVSS v4.0** | **6.9 (Medium)** — `CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:H/SC:N/SI:N/SA:N` | | **Affected** | UnrealIRCd **6.2.6** (confirmed); earlier 6.x releases shipping `rpc.add_timer`/`rpc.del_timer` likely affected | | **Discovered** | 2026-07-26, internal security research (static audit + fuzzing + gdb exploitation analysis) | | **Vendor** | UnrealIRCd Team — https://www.unrealircd.org | --- ## 1. Summary The JSON-RPC timer feature of UnrealIRCd (`rpc.add_timer`) lets an authenticated RPC user schedule an arbitrary JSON-RPC subrequest to be executed periodically by the server event loop. The event handler that iterates the timer list, `rpc_do_timers()`, prefetches the next list element and then executes the current timer's subrequest **synchronously**. If that subrequest is `rpc.del_timer` naming the timer that sits at the prefetched next position, the next element is freed mid-iteration and the loop continues to read from — and execute through — the freed `RPCTimer` structure. This is a remotely triggerable, fully deterministic heap use-after-free. It requires valid RPC credentials (`rpc-user` whose `rpc-class` permits `rpc.add_timer` and `rpc.del_timer`; the built-in `full` class does). ## 2. Root cause `src/modules/rpc/rpc.c`: ```c EVENT(rpc_do_timers) { RPCTimer *e, *e_next; for (e = rpc_timer_list; e; e = e_next) { e_next = e->next; /* (1) prefetch */ if (minimum_msec_since_last_run(&e->last_run, e->every_msec)) { rpc_call_json(e->client, e->request); /* (2) may free e_next */ } } } void free_rpc_timer(RPCTimer *r) { safe_free(r->timer_id); json_decref(r->request); DelListItem(r, rpc_timer_list); safe_free(r); /* 64-byte heap chunk */ } ``` In step (2), the timer's stored subrequest is dispatched to *any* JSON-RPC method, synchronously. When the subrequest is `{"method":"rpc.del_timer","params":{"timer_id":"<next timer>"}}`, `free_rpc_timer()` releases the `RPCTimer` that `e_next` points to. The loop then: - reads `e->next` from freed memory (line 2281), - reads `e->every_msec` and writes `e->last_run` in freed memory (line 2282), - calls `rpc_call_json(e->client, e->request)` with dangling `Client *` and `json_t *` pointers (line 2284). List ordering makes the trigger deterministic: `AddListItem()` inserts at the head of the list (`src/list.c`), so creating timer **B** first and timer **A** second yields `rpc_timer_list = [A, B]`. With both timers at `every_msec = 250` (`RPC_MINIMUM_TIMER_MSEC`), both are due in the same event pass: A fires first and deletes B (`== e_next`). Note: timers are tied to the RPC client connection and are destroyed on disconnect (`rpc_handle_free_client`), so the attack requires a persistent RPC transport, i.e. **JSON-RPC over WebSocket** (`wss://<host>:<port>/api`); plain HTTP POST closes the connection and deletes the timers before the event fires. ## 4. Impact - **Instrumented / debug builds (ASan, allocator debugging):** deterministic remote crash of the daemon — **denial of service**. - **Production builds (glibc tcache, default):** silent heap corruption. The event loop executes `rpc_call_json()` once through a freed `RPCTimer` whose `client`/`request` fields remain intact. Observed as a "zombie" invocation of the freed timer's stored request. - **Other allocators / hardened builds (no tcache, musl, jemalloc, `GLIBC_TUNABLES=glibc.malloc.tcache_count=0`):** the freed chunk is immediately reused by the `_rpc_response` `json_object()` allocation, and the loop dereferences a corrupted next pointer → remote crash. - **Code execution:** not demonstrated. Weaponization would require an attacker-influenced allocation of the 64-byte size class inside the micro-window between `free_rpc_timer()` and the loop's reuse, plus an information leak; neither primitive is currently available in the RPC response path (empirically verified with an `LD_PRELOAD` malloc interposer). The bug class (type confusion over `Client *` and `json_t *`) nevertheless makes future exploitability plausible if the allocation profile of the RPC code changes. ## 5. CVSS v4.0 scoring rationale `CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:H/SC:N/SI:N/SA:N` — **6.9** | Metric | Value | Justification | |---|---|---| | AV | Network | Triggered over the JSON-RPC listener (TCP/TLS/WebSocket) | | AC | Low | Fully deterministic trigger; no race or special heap state needed | | AT | None | No special conditions beyond a standard config with an rpc-user | | PR | Low | Requires valid `rpc-user` credentials (not IRC operator, not unauthenticated) | | UI | None | No user interaction | | VC | None | No data disclosure demonstrated | | VI | Low | Memory corruption with limited integrity effect (zombie execution from freed heap); no demonstrated control of contents | | VA | High | Daemon crash (DoS) on instrumented builds and non-tcache allocators | | SC/SI/SA | None | No demonstrated subsequent-system impact | *Note:* scores as Medium under the demonstrated impact. Should arbitrary code execution be demonstrated in the future, re-score with VC/VI:H and possible scope change (est. 8.7+). ## 6. Affected versions / prerequisites / scope - Confirmed on **UnrealIRCd 6.2.6** (the latest stable release at analysis time — the only version the vendor security policy supports). - Any 6.x version exposing `rpc.add_timer`/`rpc.del_timer` is expected to be affected. - Prerequisites: an `rpc-user { }` block and a reachable RPC listener (`listen { ... options { rpc; } }`), plus a persistent transport (JSON-RPC over WebSocket). - **Scope note (per the vendor policy):** the policy's primary scope covers issues triggerable by *regular, unauthenticated* users; this issue requires *authenticated RPC credentials*, similar to the "IRCOp rights" category, which the policy evaluates **case-by-case**. It is reported privately anyway, as the policy recommends ("better safe than sorry"): it is a genuine memory-safety violation (CWE-416) reachable over the network, with daemon-crash impact on instrumented builds and allocator-dependent impact in production. ## 7. Proposed fix Defer physical freeing while the event iterates (the "dead list" pattern already used elsewhere in the codebase): ```c EVENT(rpc_do_timers) { RPCTimer *e, *e_next; rpc_timers_iterating++; for (e = rpc_timer_list; e; e = e_next) { e_next = e->next; if (e->deleted) continue; if (minimum_msec_since_last_run(&e->last_run, e->every_msec)) rpc_call_json(e->client, e->request); } if (--rpc_timers_iterating == 0) sweep_deleted_rpc_timers(); /* real free happens here */ } void free_rpc_timer(RPCTimer *r) { if (rpc_timers_iterating) { r->deleted = 1; DelListItem(r, rpc_timer_list); AddListItem(r, rpc_timer_deadlist); return; } /* ... existing free path ... */ } ``` The same treatment should cover the self-deletion case (`rpc.del_timer` naming the timer's own id), which frees the `json_t` request currently being handled (analyzed as a second, related lifetime bug). | ||||
| Steps To Reproduce | ## 3. Reproduction (PoC) *Per the UnrealIRCd security policy for tool/AI-assisted reports: this issue was reproduced on a locally running UnrealIRCd 6.2.6 built with AddressSanitizer (`./Config` sanitizer question / `SANITIZER="asan"` in `config.settings`). The reproducer script and full ASan output are attached (`poc_rpc_timer_uaf.py`, `asan_stderr.log`).* Steps (against the ASan build): ``` 1. wss connect to /api with rpc-user credentials 2. {"method":"rpc.add_timer","params":{"timer_id":"B","every_msec":250, "request":{"method":"rpc.info","id":1}}} 3. {"method":"rpc.add_timer","params":{"timer_id":"A","every_msec":250, "request":{"method":"rpc.del_timer","params":{"timer_id":"B"},"id":2}}} 4. Within ~250 ms the rpc_do_timers event fires: A executes rpc.del_timer("B") -> free_rpc_timer(B) loop continues with e == freed B ``` ASan output (verbatim, trimmed): ``` ==15029==ERROR: AddressSanitizer: heap-use-after-free on address 0x768935c60128 READ of size 8 at 0x768935c60128 thread T0 #0 0x7629337cc2f3 in rpc_do_timers src/modules/rpc/rpc.c:2281 #1 0x5c66f32d7ce5 in DoEvents src/api-event.c:200 #2 0x5c66f310fe37 in SocketLoop src/ircd.c:952 #3 0x5c66f31077df in main src/ircd.c:928 0x768935c60128 is located 8 bytes inside of 64-byte region [0x768935c60120,0x768935c60160) freed by thread T0 here: #1 0x7629337cf396 in rpc_rpc_del_timer src/modules/rpc/rpc.c:2312 #2 0x7629337cadc2 in rpc_call_json src/modules/rpc/rpc.c:1235 #3 0x7629337cc250 in rpc_do_timers src/modules/rpc/rpc.c:2284 previously allocated by thread T0 here: #1 0x5c66f328ff60 in safe_alloc src/support.c:765 #2 0x7629337cecd0 in rpc_rpc_add_timer src/modules/rpc/rpc.c:2263 SUMMARY: AddressSanitizer: heap-use-after-free src/modules/rpc/rpc.c:2281 in rpc_do_timers ``` Result (production build, glibc 2.39): the freed timer is executed once by the event loop from freed heap ("zombie fire"), observable on the wire as an unsolicited RPC error frame produced by `rpc_call_json()` parsing the freed request tree. No crash occurs with stock glibc tcache (see §5). | ||||
| Attached Files | ADVISORY.txt (9,574 bytes) | ||||
| 3rd party modules | |||||
|
|
Thanks. I only noticed the report just now, after I got back from vacation an hour ago. I will take a look at fixing this. |
|
|
Alright, I could reproduce the issue. So the report is valid :) I will fix it straight away, openly, not delay it to some sort of security release, given that: 1) this bug requires one to be an authenticated RPC user, typically this is the web panel, which already has lots of powers that could do massive disruptions on a network and possibly impact server availability -- similar to like IRCOps but a tad more powerful even 2) i could - just like you - only demonstrate a crash impact, RCE sounds unlikely given the nature of the bug (synchronous, not many ways to interfere, etc). For the same reason I don't intend to request a CVE, since.. the issue doesn't really "buy" an attacker much, they could already mess up a server by e.g. flooding RPC calls or causing other chaos like glining everyone. I want to compliment you on the report. I mean, sure it is AI based, but the reported issue, the impact assessment, the reproducer, everything was honest, helpful and clear. |
|
|
Fixed in git https://github.com/unrealircd/unrealircd/commit/6c238ad1037675e96d9e7fa900d6faae04727e35commit 6c238ad1037675e96d9e7fa900d6faae04727e35 (HEAD -> unreal60_dev, origin/unreal60_dev, origin/HEAD) Author: Bram Matthys <[email protected]> Date: Mon Aug 17 15:33:42 2026 +0200 Fix UAF in JSON-RPC rpc.del_timer method. We tried to delete a timer immediately, but by doing so we could be accessing an already freed other timer. Instead, we now "mark for deletion" and do the actual freeing later. Very similar to what we do in EventDel actually (for non-JSON-RPC events). I am going for an immediate public git fix rather than some kind of special security release because: 1) only crash impact was demonstrated and RCE seems rather unlikely 2) this requires an authenticated JSON-RPC account, which very likely already has lots of privileges that can mess up a network (eg add a gline on *@*) or can do resource type attacks (100% CPU). After all, JSON-RPC is similar to an IRCOp in terms of access, even more powerful. This issue was reported by A. Ramos <[email protected]> in https://bugs.unrealircd.org/view.php?id=6639 Again, I want to thank you for the report. Although I don't think it has much of a security impact, it was a real crash that is good to get fixed. For example, people could be unintentionally crashing the server, and that is now fixed in git. This will be in 6.2.7 release when it gets out at some point (no ETA). I have made this bug report public now. It was good you reported it initially as 'private', that exactly how we want it to. But now that it has been analyzed and fixed, I set it to 'public'. |
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-07-26 11:45 | aramosf | New Issue | |
| 2026-07-26 11:45 | aramosf | File Added: ADVISORY.txt | |
| 2026-08-17 14:34 | syzop | Note Added: 0023699 | |
| 2026-08-17 15:30 | syzop | Note Added: 0023700 | |
| 2026-08-17 15:50 | syzop | View Status | private => public |
| 2026-08-17 15:52 | syzop | Assigned To | => syzop |
| 2026-08-17 15:52 | syzop | Status | new => resolved |
| 2026-08-17 15:52 | syzop | Resolution | open => fixed |
| 2026-08-17 15:52 | syzop | Fixed in Version | => 6.2.7 |
| 2026-08-17 15:52 | syzop | Note Added: 0023701 | |
| 2026-08-17 15:53 | syzop | Note Edited: 0023701 |