Skip to content

Commit a11c735

Browse files
committed
fix(lua): address code-review findings on PR #857 (C1/C2/C3 + H2/H4/M1)
Verified each against source before fixing. C1 (data loss / hang): l_conn_read* issued hio_read_until_* BEFORE arming the read callback + suspending. hio_read_until_length/delim invoke the read callback INLINE when buffered data already satisfies the request (hevent.c:765/789), at which point c->co is unset -> on_conn_read dropped the data and the coroutine suspended forever. Now arm callback + enter a 'reading' window first, issue the read, and if it completed synchronously return the data directly without suspending (mirrors the redis RedisCmdState pattern). Regression-tested with pipelined readbytes. C2 (UAF + double-free): a fired once-timer's htimer_t is freed by the loop after its callback, but on_lua_timer freed LuaTimer without clearing the timer's back-pointer, and the script still held the raw handle. clearTimer(stale) then read hevent_userdata of freed memory -> UAF + double-free. Add a live-timer registry; clearTimer validates the handle against it (stale -> safe no-op), and lua_timer_free unregisters + clears hevent_userdata. C3 (UAF): mqtt_client_free freed cli without closing cli->io or deleting cli->timer or clearing their hevent_userdata(cli); a later on_close / connect_timeout_cb / reconnect_timer_cb dereferenced the freed cli. Tear down io (detach close cb + close) and timer (detach + del) inside mqtt_client_free. H2 (UAF): ~WebSocketClient didn't clear channel->onclose, so ~Channel -> close() fired the onclose lambda capturing the destroying TcpClient. Clear it first (mirrors AsyncHttpClient::~AsyncHttpClient). H4 (leak): on_dns_resolved returned without hvlua_cancel when the coroutine was gone, leaking the suspend token. Cancel it. M1 (CI gap): scripts/unittest.sh ran lua_http_test but not lua_ws_test / lua_mqtt_test / lua_redis_test. Add them. Verified: full lua/redis suite (12 tests) passes under Make; clean rebuild.
1 parent 0945b2b commit a11c735

4 files changed

Lines changed: 151 additions & 11 deletions

File tree

http/client/WebSocketClient.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ WebSocketClient::WebSocketClient(EventLoopPtr loop)
1616
}
1717

1818
WebSocketClient::~WebSocketClient() {
19+
// Detach the channel's close callback before teardown. ~TcpClientTmpl (base)
20+
// and the channel dtor run ~Channel -> close() -> the onclose lambda, which
21+
// was set to [this]{ notifyDisconnectThenReconnect(); } and captures this
22+
// TcpClient. During destruction (e.g. a Lua __gc dropping a still-open ws)
23+
// that would touch a partially-destroyed object (UAF). Clearing onclose makes
24+
// Channel::on_close a no-op (mirrors AsyncHttpClient::~AsyncHttpClient).
25+
if (channel) {
26+
channel->onclose = NULL;
27+
}
1928
stop();
2029
}
2130

lua/hvlua_event.c

Lines changed: 120 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,59 @@ static void lua_timer_release_ref(LuaTimer* lt) {
3636
}
3737
}
3838

39+
// Live-timer registry: registry["hv.timers"][lightuserdata(timer)] = true.
40+
// hv.clearTimer receives a raw htimer_t* as an opaque handle, but a fired
41+
// once-timer's htimer_t is freed by the loop after its callback, leaving the
42+
// script holding a stale pointer. Validating the handle against this registry
43+
// (instead of blindly dereferencing hevent_userdata) makes clearTimer on a
44+
// stale/already-freed handle a safe no-op.
45+
#define HVLUA_TIMERS_REG "hv.timers"
46+
47+
static void lua_timers_reg_get(lua_State* L) {
48+
lua_getfield(L, LUA_REGISTRYINDEX, HVLUA_TIMERS_REG);
49+
if (!lua_istable(L, -1)) {
50+
lua_pop(L, 1);
51+
lua_newtable(L);
52+
lua_pushvalue(L, -1);
53+
lua_setfield(L, LUA_REGISTRYINDEX, HVLUA_TIMERS_REG);
54+
}
55+
}
56+
57+
static void lua_timer_reg_add(lua_State* L, htimer_t* timer) {
58+
lua_timers_reg_get(L); // [timers]
59+
lua_pushlightuserdata(L, timer); // [timers][key]
60+
lua_pushboolean(L, 1); // [timers][key][true]
61+
lua_settable(L, -3); // timers[key]=true
62+
lua_pop(L, 1);
63+
}
64+
65+
static void lua_timer_reg_remove(lua_State* L, htimer_t* timer) {
66+
if (timer == NULL) return;
67+
lua_timers_reg_get(L); // [timers]
68+
lua_pushlightuserdata(L, timer); // [timers][key]
69+
lua_pushnil(L); // [timers][key][nil]
70+
lua_settable(L, -3); // timers[key]=nil
71+
lua_pop(L, 1);
72+
}
73+
74+
// Is `timer` a currently-live lua timer handle?
75+
static int lua_timer_reg_has(lua_State* L, htimer_t* timer) {
76+
int has;
77+
lua_timers_reg_get(L); // [timers]
78+
lua_pushlightuserdata(L, timer); // [timers][key]
79+
lua_gettable(L, -2); // [timers][val]
80+
has = lua_toboolean(L, -1);
81+
lua_pop(L, 2);
82+
return has;
83+
}
84+
3985
static void lua_timer_free(LuaTimer* lt) {
86+
// Unregister the handle so a later clearTimer on this (soon-freed) timer is
87+
// a safe no-op, and detach the timer's back-pointer to lt.
88+
if (lt->timer) {
89+
lua_timer_reg_remove(lt->L, lt->timer);
90+
hevent_set_userdata(lt->timer, NULL);
91+
}
4092
lua_timer_release_ref(lt);
4193
HV_FREE(lt);
4294
}
@@ -100,6 +152,7 @@ static htimer_t* add_lua_timer(lua_State* L, uint32_t timeout_ms, uint32_t repea
100152
}
101153
hevent_set_userdata(timer, lt);
102154
lt->timer = timer;
155+
lua_timer_reg_add(L, timer); // mark handle live for clearTimer validation
103156
return timer;
104157
}
105158

@@ -128,13 +181,20 @@ static int l_hloop_clearTimer(lua_State* L) {
128181
if (!lua_islightuserdata(L, 1)) return 0;
129182
timer = (htimer_t*)lua_touserdata(L, 1);
130183
if (timer == NULL) return 0;
184+
// Validate the handle: a fired once-timer's htimer_t is freed by the loop,
185+
// so the script may hold a stale pointer. Only proceed if the handle is a
186+
// currently-live timer we registered; otherwise it's a safe no-op (avoids
187+
// dereferencing freed memory via hevent_userdata).
188+
if (!lua_timer_reg_has(L, timer)) return 0;
131189
lt = (LuaTimer*)hevent_userdata(timer);
132190
htimer_del(timer);
133191
if (lt) {
134192
if (lt->in_callback) {
135193
// Called from within this timer's own callback: defer the free to
136-
// on_lua_timer so we don't free `lt` while it's still in use.
194+
// on_lua_timer so we don't free `lt` while it's still in use. Drop it
195+
// from the live registry now so no further clearTimer can match.
137196
lt->dead = 1;
197+
lua_timer_reg_remove(L, timer);
138198
lua_timer_release_ref(lt);
139199
} else {
140200
lua_timer_free(lt);
@@ -208,7 +268,8 @@ static void on_dns_resolved(hdns_t* query, const hdns_result_t* result, void* us
208268
HV_FREE(d);
209269

210270
L = hvlua_coroutine_state(co);
211-
if (L == NULL) { // coroutine gone (loop teardown); nothing to resume
271+
if (L == NULL) { // coroutine gone (loop teardown); release the token
272+
hvlua_cancel(co);
212273
return;
213274
}
214275

@@ -289,6 +350,14 @@ typedef struct LuaConn {
289350
int closed; // set in the close callback
290351
int connecting; // pending op is connect (vs read), for resume shape
291352
unpack_setting_t* unpack; // owned; hio_t only stores the pointer
353+
// Sync-read detection: hio_read_until_length/delim may invoke the read
354+
// callback INLINE (buffered data already satisfies the request) before the
355+
// coroutine yields. In that window co is not yet set, so on_conn_read must
356+
// not resume; instead it stashes the data here and l_conn_read* returns it
357+
// directly without suspending.
358+
lua_State* reading_L; // non-NULL while arming a read (the running coroutine)
359+
int read_done; // set true if the read completed synchronously
360+
int read_nres; // number of results pushed on reading_L (sync path)
292361
} LuaConn;
293362

294363
// forward decl: conn_push_new is defined lower (after the read helpers) but
@@ -338,7 +407,18 @@ static void on_conn_connect(hio_t* io) {
338407

339408
static void on_conn_read(hio_t* io, void* buf, int len) {
340409
LuaConn* c = (LuaConn*)hevent_userdata(io);
341-
if (c == NULL || c->co == NULL) return;
410+
if (c == NULL) return;
411+
// Synchronous completion: hio_read_until_* found buffered data and called
412+
// us inline, before the coroutine yielded. co is not set yet; push the data
413+
// onto the running coroutine and let l_conn_read* return it directly (do NOT
414+
// resume — the coroutine is still running).
415+
if (c->reading_L) {
416+
lua_pushlstring(c->reading_L, (const char*)buf, len);
417+
c->read_done = 1;
418+
c->read_nres = 1;
419+
return;
420+
}
421+
if (c->co == NULL) return;
342422
lua_State* co = hvlua_coroutine_state(c->co);
343423
if (co == NULL) { hvlua_cancel(c->co); c->co = NULL; return; }
344424
lua_pushlstring(co, (const char*)buf, len);
@@ -397,6 +477,9 @@ static LuaConn* conn_push_new(lua_State* L, hio_t* io) {
397477
c->closed = 0;
398478
c->connecting = 0;
399479
c->unpack = NULL;
480+
c->reading_L = NULL;
481+
c->read_done = 0;
482+
c->read_nres = 0;
400483
luaL_getmetatable(L, CONN_META);
401484
lua_setmetatable(L, -2);
402485
hevent_set_userdata(io, c);
@@ -410,10 +493,33 @@ static int read_k(lua_State* L, int status, lua_KContext ctx) {
410493
return lua_gettop(L) >= 2 && lua_isnil(L, -2) ? 2 : 1;
411494
}
412495

413-
// Shared: arm the read callback + suspend. The specific hio_read* call is done
414-
// by the caller (read once / read_until_delim / read_until_length).
415-
static int conn_suspend_read(lua_State* L, LuaConn* c) {
496+
// Read protocol (fixes sync-callback data loss): the hio_read_until_* calls may
497+
// invoke on_conn_read INLINE when buffered data already satisfies the request.
498+
// So we (1) arm the read callback and mark reading_L BEFORE issuing hio_read*,
499+
// (2) issue hio_read*, then (3) if the callback fired synchronously (read_done),
500+
// return the data directly; otherwise suspend the coroutine.
501+
//
502+
// conn_begin_read: arm callback + enter the "reading" window. Returns 0 on ok.
503+
static void conn_begin_read(lua_State* L, LuaConn* c) {
416504
hio_setcb_read(c->io, on_conn_read);
505+
c->reading_L = L;
506+
c->read_done = 0;
507+
c->read_nres = 0;
508+
}
509+
510+
// conn_end_read: leave the reading window; return results if the read completed
511+
// synchronously, else suspend the coroutine and yield.
512+
static int conn_end_read(lua_State* L, LuaConn* c) {
513+
c->reading_L = NULL;
514+
if (c->read_done) {
515+
// Data already pushed on L by on_conn_read; return it directly.
516+
return c->read_nres;
517+
}
518+
// Also handle the case where the read synchronously closed the connection
519+
// (on_conn_close ran inline and set closed): report it without suspending.
520+
if (c->closed || c->io == NULL) {
521+
lua_pushnil(L); lua_pushstring(L, "closed"); return 2;
522+
}
417523
c->co = hvlua_suspend(L);
418524
return lua_yieldk(L, 0, (lua_KContext)0, read_k);
419525
}
@@ -424,8 +530,9 @@ static int l_conn_read(lua_State* L) {
424530
if (c->closed || c->io == NULL) {
425531
lua_pushnil(L); lua_pushstring(L, "closed"); return 2;
426532
}
533+
conn_begin_read(L, c);
427534
hio_read(c->io);
428-
return conn_suspend_read(L, c);
535+
return conn_end_read(L, c);
429536
}
430537

431538
// conn:readbytes(n) -> exactly n bytes (hio_read_until_length)
@@ -435,8 +542,9 @@ static int l_conn_readbytes(lua_State* L) {
435542
if (c->closed || c->io == NULL) {
436543
lua_pushnil(L); lua_pushstring(L, "closed"); return 2;
437544
}
545+
conn_begin_read(L, c);
438546
hio_read_until_length(c->io, n);
439-
return conn_suspend_read(L, c);
547+
return conn_end_read(L, c);
440548
}
441549

442550
// conn:readuntil(delim) -> data up to and including the 1-byte delimiter
@@ -450,8 +558,9 @@ static int l_conn_readuntil(lua_State* L) {
450558
if (dlen != 1) {
451559
return luaL_error(L, "conn:readuntil expects a single-byte delimiter");
452560
}
561+
conn_begin_read(L, c);
453562
hio_read_until_delim(c->io, (unsigned char)delim[0]);
454-
return conn_suspend_read(L, c);
563+
return conn_end_read(L, c);
455564
}
456565

457566
// conn:readline() -> data up to and including '\n'
@@ -460,8 +569,9 @@ static int l_conn_readline(lua_State* L) {
460569
if (c->closed || c->io == NULL) {
461570
lua_pushnil(L); lua_pushstring(L, "closed"); return 2;
462571
}
572+
conn_begin_read(L, c);
463573
hio_read_until_delim(c->io, '\n');
464-
return conn_suspend_read(L, c);
574+
return conn_end_read(L, c);
465575
}
466576

467577
// conn:setUnpack(opts): configure automatic message unpacking so subsequent

mqtt/mqtt_client.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,21 @@ mqtt_client_t* mqtt_client_new(hloop_t* loop) {
457457

458458
void mqtt_client_free(mqtt_client_t* cli) {
459459
if (!cli) return;
460+
// Tear down the io and timer BEFORE freeing cli. They were registered on the
461+
// loop with hevent_set_userdata(..., cli); if left armed, a later on_close /
462+
// connect_timeout_cb / reconnect_timer_cb would dereference the freed cli
463+
// (use-after-free). Detach their back-pointers and close/delete them first.
464+
if (cli->timer) {
465+
hevent_set_userdata(cli->timer, NULL);
466+
htimer_del(cli->timer);
467+
cli->timer = NULL;
468+
}
469+
if (cli->io) {
470+
hevent_set_userdata(cli->io, NULL);
471+
hio_setcb_close(cli->io, NULL); // no on_close callback into freed cli
472+
hio_close(cli->io);
473+
cli->io = NULL;
474+
}
460475
hmutex_destroy(&cli->mutex_);
461476
if (cli->ssl_ctx && cli->alloced_ssl_ctx) {
462477
hssl_ctx_free(cli->ssl_ctx);

scripts/unittest.sh

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,19 @@ fi
4747
if [ -x bin/lua_http_test ]; then
4848
bin/lua_http_test
4949
fi
50+
if [ -x bin/lua_ws_test ]; then
51+
bin/lua_ws_test
52+
fi
53+
if [ -x bin/lua_mqtt_test ]; then
54+
bin/lua_mqtt_test
55+
fi
5056
if [ -x bin/hdns_test ]; then
5157
bin/hdns_test
5258
fi
5359
if [ -x bin/tcpclient_dns_test ]; then
5460
bin/tcpclient_dns_test
5561
fi
56-
for redis_test in redis_async_client_test redis_client_test redis_batch_test redis_subscriber_test; do
62+
for redis_test in redis_async_client_test redis_client_test redis_batch_test redis_subscriber_test lua_redis_test; do
5763
if [ -x bin/${redis_test} ]; then
5864
bin/${redis_test}
5965
fi

0 commit comments

Comments
 (0)