Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Audit string coercion throughout luv #397

Open
squeek502 opened this issue Oct 9, 2019 · 1 comment
Open

Audit string coercion throughout luv #397

squeek502 opened this issue Oct 9, 2019 · 1 comment
Labels

Comments

@squeek502
Copy link
Member

squeek502 commented Oct 9, 2019

From #127 (comment)

Another unrelated thing (which probably requires a separate issue to be opened) is that one must be very careful when converting stack values to strings via any of lua_tostring, luaL_checkstring, luaL_optstring and other functions that may cause coercion. For example, I can see that in your code when a table of strings is written to a stream, there is no protection that an element might in fact be a number. When a uv_buf_t vector is being initialized, the item is converted to a string on the stack and its pointer is saved in the vector. All seems good. However, after the conversion is made and before the actual string is written to a socket in a uv_write_t request, garbage collection may happen effectively invalidating the saved pointer because its source string existed briefly on the stack as a result of coercion (the item in the table is still a number, not a string). And that means in turn that undefined data will actually be written.

Btw, the table that is used to form a uv_buf_t vector may be altered even after the call directly in Lua which will lead to the same outcome. There is no protection against it.

I haven't checked through your code much, but there are many other caveats when using Lua C API that one must be aware of. They are all more or less mentioned in the manual though.

This is something I ran into when writing #393 (if lua_isstring was the first if check, then a number passed as the argument would return true from lua_isstring and get coerced into a string) and it's something that we need to be more careful of and are probably using incorrectly.

@squeek502 squeek502 added the ? label Oct 9, 2019
@squeek502
Copy link
Member Author

squeek502 commented Oct 28, 2019

Confirmed that the uv_buf_t problem mentioned in the quote above is accurate and reproducible via:

local uv = require('luv')

local path = "_test_"
local fd = assert(uv.fs_open(path, "w", tonumber("0666", 8)))
print("writing")
do
  -- the number here gets coerced into a string
  local t = {"with\n", 600, "lines\n"}
  uv.fs_write(fd, t, -1, function()
    print("written")
    uv.fs_close(fd)
  end)
end
local count = collectgarbage("count")
collectgarbage("collect")
print("garbage collected", count - collectgarbage("count"))

uv.run()

Note that it's the coerced number that is failing; I couldn't get a crash when using strings (probably due to how strings are handled internally by Lua). When I run the above with PUC Lua 5.1 and Valgrind I get:

==13484== Memcheck, a memory error detector
==13484== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==13484== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==13484== Command: lua test.lua
==13484==
writing
garbage collected       1.017578125
==13484== Thread 2:
==13484== Syscall param writev(vector[...]) points to unaddressable byte(s)
==13484==    at 0x568AFFD: ??? (syscall-template.S:84)
==13484==    by 0x5FB5C15: uv__fs_write (in /home/ryan/Programming/luv/build/luv.so)
==13484==    by 0x5FB68EC: uv__fs_write_all (in /home/ryan/Programming/luv/build/luv.so)
==13484==    by 0x5FB6E6E: uv__fs_work (in /home/ryan/Programming/luv/build/luv.so)
==13484==    by 0x5FAB950: worker (in /home/ryan/Programming/luv/build/luv.so)
==13484==    by 0x63F06B9: start_thread (pthread_create.c:333)
==13484==  Address 0x5bc3968 is 24 bytes inside a block of size 28 free'd
==13484==    at 0x4C2EF90: free (vg_replace_malloc.c:540)
==13484==    by 0x4123F8: ??? (in /usr/bin/lua5.1)
==13484==    by 0x40A6DD: ??? (in /usr/bin/lua5.1)
==13484==    by 0x40968D: ??? (in /usr/bin/lua5.1)
==13484==    by 0x40A010: ??? (in /usr/bin/lua5.1)
==13484==    by 0x40A577: ??? (in /usr/bin/lua5.1)
==13484==    by 0x405EF4: lua_gc (in /usr/bin/lua5.1)
==13484==    by 0x4172A1: ??? (in /usr/bin/lua5.1)
==13484==    by 0x4080DF: ??? (in /usr/bin/lua5.1)
==13484==    by 0x4116E1: ??? (in /usr/bin/lua5.1)
==13484==    by 0x40853C: ??? (in /usr/bin/lua5.1)
==13484==    by 0x40782A: ??? (in /usr/bin/lua5.1)
==13484==  Block was alloc'd at
==13484==    at 0x4C2DDE0: malloc (vg_replace_malloc.c:308)
==13484==    by 0x4C30130: realloc (vg_replace_malloc.c:836)
==13484==    by 0x40A6DD: ??? (in /usr/bin/lua5.1)
==13484==    by 0x40E295: ??? (in /usr/bin/lua5.1)
==13484==    by 0x410296: ??? (in /usr/bin/lua5.1)
==13484==    by 0x4051D2: lua_tolstring (in /usr/bin/lua5.1)
==13484==    by 0x41296E: luaL_checklstring (in /usr/bin/lua5.1)
==13484==    by 0x5F9680E: luv_check_buf (in /home/ryan/Programming/luv/build/luv.so)
==13484==    by 0x5F96F1C: luv_prep_bufs (in /home/ryan/Programming/luv/build/luv.so)
==13484==    by 0x5F9C325: luv_fs_write (in /home/ryan/Programming/luv/build/luv.so)
==13484==    by 0x4080DF: ??? (in /usr/bin/lua5.1)
==13484==    by 0x4116E1: ??? (in /usr/bin/lua5.1)
==13484==
written
==13484==
==13484== HEAP SUMMARY:
==13484==     in use at exit: 3,313 bytes in 10 blocks
==13484==   total heap usage: 1,599 allocs, 1,589 frees, 163,941 bytes allocated
==13484==
==13484== LEAK SUMMARY:
==13484==    definitely lost: 0 bytes in 0 blocks
==13484==    indirectly lost: 0 bytes in 0 blocks
==13484==      possibly lost: 0 bytes in 0 blocks
==13484==    still reachable: 3,313 bytes in 10 blocks
==13484==         suppressed: 0 bytes in 0 blocks
==13484== Rerun with --leak-check=full to see details of leaked memory
==13484==
==13484== For lists of detected and suppressed errors, rerun with: -s
==13484== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

squeek502 added a commit to squeek502/luv that referenced this issue Oct 28, 2019
When a number is coerced to a string, that string is temporary and could be garbage collected before the uv_buf_t is used, leading to use-after-free.

Part of luvit#397
squeek502 added a commit to squeek502/luv that referenced this issue Oct 31, 2019
Part of luvit#397

luv_req_t now stores either one or multiple refs to the strings passed to functions that use uv_buf_t. This fixes those strings possibly being garbage collected between the time the uv_buf_t is constructed and the time the send actually completes. See luvit#397 for a more complete explanation of the problem.

- Introduces a constant called LUV_REQ_MULTIREF which is an arbitrary negative value that luaL_ref will never return that signifies that luv_req_t.data is a LUA_NOREF-terminated array of `int`s and handles that case accordingly in luv_cleanup_req. This might be more hacky than it needs to be but it makes it possible to leave the structure of luv_req_t unchanged.
- Maintains backwards compatibility in that it will still coerce numbers to strings (see luvit#426)
- Factored out and moved `uv_buf_t`-related functions to misc.c (matches where uv_buf_t is in Libuv: http://docs.libuv.org/en/v1.x/misc.html#c.uv_buf_t)
  + Has the side-benefit of bringing udp_send/udp_try_send in line with the other send/write functions in that it will now accept either a string or a table of strings (before the udp send functions only accepted a single string)
squeek502 added a commit to squeek502/luv that referenced this issue Oct 31, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant