Skip to content

Commit 7233e6d

Browse files
authored
Merge pull request #696 from squeek502/scandir-gc
Fix garbage collection of scandir reqs
2 parents 343b51b + 4b80697 commit 7233e6d

File tree

5 files changed

+94
-14
lines changed

5 files changed

+94
-14
lines changed

src/fs.c

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,14 @@ typedef struct {
2424
} luv_dir_t;
2525
#endif
2626

27+
typedef struct {
28+
uv_fs_t* req;
29+
} luv_fs_scandir_t;
30+
2731
static uv_fs_t* luv_check_fs(lua_State* L, int index) {
28-
if (luaL_testudata(L, index, "uv_fs") != NULL) {
29-
return (uv_fs_t*)lua_touserdata(L, index);
32+
if (luaL_testudata(L, index, "uv_fs_scandir") != NULL) {
33+
luv_fs_scandir_t* scandir_req = (luv_fs_scandir_t*)lua_touserdata(L, index);
34+
return (uv_fs_t*)(scandir_req->req);
3035
}
3136
uv_fs_t* req = (uv_fs_t*)luaL_checkudata(L, index, "uv_req");
3237
luaL_argcheck(L, req->type == UV_FS && req->data, index, "Expected uv_fs_t");
@@ -251,6 +256,13 @@ static int push_fs_result(lua_State* L, uv_fs_t* req) {
251256
}
252257

253258
if (req->result < 0) {
259+
if (req->fs_type == UV_FS_SCANDIR) {
260+
// We need to unref the luv_fs_scandir_t userdata to allow it to be garbage collected.
261+
// The scandir callback can only be called once, so we now know that the
262+
// req can be safely garbage collected.
263+
luaL_unref(L, LUA_REGISTRYINDEX, data->data_ref);
264+
data->data_ref = LUA_NOREF;
265+
}
254266
lua_pushnil(L);
255267
if (fs_req_has_dest_path(req)) {
256268
lua_rawgeti(L, LUA_REGISTRYINDEX, data->data_ref);
@@ -336,8 +348,15 @@ static int push_fs_result(lua_State* L, uv_fs_t* req) {
336348
return 1;
337349

338350
case UV_FS_SCANDIR:
339-
// Expose the userdata for the request.
340-
lua_rawgeti(L, LUA_REGISTRYINDEX, data->req_ref);
351+
// The luv_fs_scandir_t userdata is stored in data_ref.
352+
// We want to return this instead of the uv_req_t because the
353+
// luv_fs_scandir_t userdata has a gc method.
354+
lua_rawgeti(L, LUA_REGISTRYINDEX, data->data_ref);
355+
// We now want to unref the userdata to allow it to be garbage collected.
356+
// The scandir callback can only be called once, so we now know that the
357+
// req can be safely garbage collected.
358+
luaL_unref(L, LUA_REGISTRYINDEX, data->data_ref);
359+
data->data_ref = LUA_NOREF;
341360
return 1;
342361

343362
#if LUV_UV_VERSION_GEQ(1, 28, 0)
@@ -470,9 +489,6 @@ static void luv_fs_cb(uv_fs_t* req) {
470489
luv_cleanup_req(L, lreq); \
471490
req->data = NULL; \
472491
uv_fs_req_cleanup(req); \
473-
} else { \
474-
luaL_unref(L, LUA_REGISTRYINDEX, lreq->req_ref); \
475-
lreq->req_ref = LUA_NOREF; \
476492
} \
477493
} \
478494
else { \
@@ -614,8 +630,34 @@ static int luv_fs_scandir(lua_State* L) {
614630
int flags = 0; // TODO: find out what these flags are.
615631
int ref = luv_check_continuation(L, 2);
616632
uv_fs_t* req = (uv_fs_t*)lua_newuserdata(L, uv_req_size(UV_FS));
617-
req->data = luv_setup_req_with_mt(L, ctx, ref, "uv_fs");
618-
FS_CALL(uv_fs_scandir, req, path, flags);
633+
req->data = luv_setup_req(L, ctx, ref);
634+
int sync = ref == LUA_NOREF;
635+
636+
// Wrap the req in a garbage-collectable wrapper.
637+
// This allows us to separate the lifetime of the uv_req_t from the lifetime
638+
// of the userdata that gets returned here, since otherwise the returned uv_req_t
639+
// would hold a reference to itself making it ineligible for garbage collection before
640+
// the process ends.
641+
luv_fs_scandir_t* scandir_req = (luv_fs_scandir_t*)lua_newuserdata(L, sizeof(luv_fs_scandir_t));
642+
scandir_req->req = req;
643+
luaL_getmetatable(L, "uv_fs_scandir");
644+
lua_setmetatable(L, -2);
645+
int scandir_req_index = lua_gettop(L);
646+
647+
int nargs;
648+
FS_CALL_NORETURN(uv_fs_scandir, req, path, flags);
649+
// This indicates an error, so we want to return immediately
650+
if (nargs != 1) return nargs;
651+
652+
// Ref the return if this is async, since we don't want this to be garbage collected
653+
// before the callback is called.
654+
if (!sync) {
655+
lua_pushvalue(L, scandir_req_index);
656+
((luv_req_t*)req->data)->data_ref = luaL_ref(L, LUA_REGISTRYINDEX);
657+
}
658+
659+
lua_pushvalue(L, scandir_req_index);
660+
return 1;
619661
}
620662

621663
static int luv_fs_scandir_next(lua_State* L) {

src/luv.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -674,9 +674,8 @@ static void luv_req_init(lua_State* L) {
674674
lua_setfield(L, -2, "__index");
675675
lua_pop(L, 1);
676676

677-
// Only used for things that need to be garbage collected
678-
// (e.g. the req when using uv_fs_scandir)
679-
luaL_newmetatable(L, "uv_fs");
677+
// Only used for luv_fs_scandir_t
678+
luaL_newmetatable(L, "uv_fs_scandir");
680679
lua_pushcfunction(L, luv_req_tostring);
681680
lua_setfield(L, -2, "__tostring");
682681
luaL_newlib(L, luv_req_methods);

src/req.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
#include "private.h"
1818

1919
static uv_req_t* luv_check_req(lua_State* L, int index) {
20-
if (luaL_testudata(L, index, "uv_fs") != NULL) {
21-
return (uv_req_t*)lua_touserdata(L, index);
20+
if (luaL_testudata(L, index, "uv_fs_scandir") != NULL) {
21+
luv_fs_scandir_t* scandir_req = (luv_fs_scandir_t*)lua_touserdata(L, index);
22+
return (uv_req_t*)(scandir_req->req);
2223
}
2324
uv_req_t* req = (uv_req_t*)luaL_checkudata(L, index, "uv_req");
2425
luaL_argcheck(L, req->data, index, "Expected uv_req_t");

tests/manual-test-leaks.lua

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,30 @@ return require('lib/tap')(function (test)
3939
end)
4040
end)
4141

42+
test("fs-scandir", function (print, p, expect, uv)
43+
bench(uv, p, 0x10000, function ()
44+
local req = assert(uv.fs_scandir('.'))
45+
end)
46+
end)
47+
48+
test("fs-scandir-async", function (print, p, expect, uv)
49+
bench(uv, p, 0x10000, function ()
50+
local outer_req = assert(uv.fs_scandir('.', function(err, req)
51+
assert(not err)
52+
assert(req)
53+
end))
54+
end)
55+
end)
56+
57+
test("fs-scandir-async error", function (print, p, expect, uv)
58+
bench(uv, p, 0x10000, function ()
59+
local outer_req = assert(uv.fs_scandir('intentionally missing folder this should not exist!', function(err, req)
60+
assert(err)
61+
assert(not req)
62+
end))
63+
end)
64+
end)
65+
4266
test("lots-o-timers", function (print, p, expect, uv)
4367
bench(uv, p, 0x100000, function ()
4468
local timer = uv.new_timer()

tests/test-fs.lua

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,20 @@ return require('lib/tap')(function (test)
171171
assert(req)
172172
end)
173173

174+
-- this previously hit a use-after-free
175+
-- see https://github.com/luvit/luv/pull/696
176+
test("fs.scandir given to new_work", function(print, p, expect, uv)
177+
local req = assert(uv.fs_scandir('.'))
178+
local work
179+
work = assert(uv.new_work(function(_entries)
180+
local _uv = require('luv')
181+
while true do
182+
if not _uv.fs_scandir_next(_entries) then break end
183+
end
184+
end, function() end))
185+
work:queue(req)
186+
end)
187+
174188
test("fs.realpath", function (print, p, expect, uv)
175189
p(assert(uv.fs_realpath('.')))
176190
assert(uv.fs_realpath('.', expect(function (err, path)

0 commit comments

Comments
 (0)