From 5576ee3b6c73bb828c4492cd9e0e21ee3778dc95 Mon Sep 17 00:00:00 2001
From: Nikolay Shirokovskiy <nshirokovskiy@tarantool.org>
Date: Tue, 4 Jul 2023 12:56:15 +0300
Subject: [PATCH] asan: prepare for ASAN-friendly ibuf

ASAN-friendly implementation poisons memory after allocation with
ibuf_alloc so we need to fix existing places in code where we access
memory after allocation.

Part of ibuf implementation is inline functions in headers. Thus ibuf
implementation in Lua reimplement this parts. We add poison to these
inline functions in ASAN-friedly implementation so we need add same poison
in Lua implementation.

Part of #7327

NO_CHANGELOG=internal
NO_DOC=internal

(cherry picked from commit 4f542bb7f79a33f114d2bdfdf3e84ab8c3299f48)
---
 src/box/iproto.cc                |  3 +-
 src/lua/buffer.lua               | 18 +++++++
 src/lua/init.c                   |  2 +-
 src/lua/utils.lua                | 33 +++++++++++++
 test/app-luatest/buffer_test.lua | 81 ++++++++++++++++++++++++++++++++
 test/box-tap/merger.test.lua     |  4 +-
 6 files changed, 137 insertions(+), 4 deletions(-)
 create mode 100644 test/app-luatest/buffer_test.lua

diff --git a/src/box/iproto.cc b/src/box/iproto.cc
index a1419233b9..f2952812f0 100644
--- a/src/box/iproto.cc
+++ b/src/box/iproto.cc
@@ -1322,6 +1322,7 @@ iproto_connection_on_input(ev_loop *loop, struct ev_io *watcher,
 			return;
 		}
 		/* Read input. */
+		ibuf_reserve(in, ibuf_unused(in));
 		ssize_t nrd = iostream_read(io, in->wpos, ibuf_unused(in));
 		if (nrd < 0) {                  /* Socket is not ready. */
 			if (nrd == IOSTREAM_ERROR)
@@ -1343,7 +1344,7 @@ iproto_connection_on_input(ev_loop *loop, struct ev_io *watcher,
 			      IPROTO_RECEIVED, nrd);
 
 		/* Update the read position and connection state. */
-		in->wpos += nrd;
+		ibuf_alloc(in, nrd);
 		con->parse_size += nrd;
 		/* Enqueue all requests which are fully read up. */
 		if (iproto_enqueue_batch(con, in) != 0)
diff --git a/src/lua/buffer.lua b/src/lua/buffer.lua
index c5cefe32b6..9fc5ee8fda 100644
--- a/src/lua/buffer.lua
+++ b/src/lua/buffer.lua
@@ -1,6 +1,7 @@
 -- buffer.lua (internal file)
 
 local ffi = require('ffi')
+local utils = require('internal.utils')
 local READAHEAD = 16320
 
 ffi.cdef[[
@@ -120,10 +121,19 @@ local function ibuf_recycle(buf)
     builtin.ibuf_reinit(buf)
 end
 
+local function ibuf_poison_unallocated(buf)
+    utils.poison_memory_region(buf.wpos, buf.epos - buf.wpos);
+end
+
+local function ibuf_unpoison_unallocated(buf)
+    utils.unpoison_memory_region(buf.wpos, buf.epos - buf.wpos);
+end
+
 local function ibuf_reset(buf)
     checkibuf(buf, 'reset')
     buf.rpos = buf.buf
     buf.wpos = buf.buf
+    ibuf_poison_unallocated(buf)
 end
 
 local function ibuf_reserve_slow(buf, size)
@@ -137,6 +147,7 @@ end
 local function ibuf_reserve(buf, size)
     checkibuf(buf, 'reserve')
     if buf.wpos + size <= buf.epos then
+        ibuf_unpoison_unallocated(buf)
         return buf.wpos
     end
     return ibuf_reserve_slow(buf, size)
@@ -146,11 +157,18 @@ local function ibuf_alloc(buf, size)
     checkibuf(buf, 'alloc')
     local wpos
     if buf.wpos + size <= buf.epos then
+        --
+        -- In case of using same buffer we need to unpoison newly
+        -- allocated memory after previous ibuf_alloc or poison after
+        -- newly allocated memory after previous ibuf_reserve.
+        --
+        ibuf_unpoison_unallocated(buf)
         wpos = buf.wpos
     else
         wpos = ibuf_reserve_slow(buf, size)
     end
     buf.wpos = buf.wpos + size
+    ibuf_poison_unallocated(buf)
     return wpos
 end
 
diff --git a/src/lua/init.c b/src/lua/init.c
index 354fa52e0d..10cbb816e2 100644
--- a/src/lua/init.c
+++ b/src/lua/init.c
@@ -279,6 +279,7 @@ static const char *lua_modules[] = {
 	/* Make it first to affect load of all other modules */
 	"strict", strict_lua,
 	"compat", compat_lua,
+	"internal.utils", utils_lua,
 	"fun", fun_lua,
 	"debug", debug_lua,
 	"tarantool", init_lua,
@@ -303,7 +304,6 @@ static const char *lua_modules[] = {
 	"tap", tap_lua,
 	"help.en_US", help_en_US_lua,
 	"help", help_lua,
-	"internal.utils", utils_lua,
 	"internal.argparse", argparse_lua,
 	"internal.trigger", trigger_lua,
 	"pwd", pwd_lua,
diff --git a/src/lua/utils.lua b/src/lua/utils.lua
index 53797e2834..b69d64ef45 100644
--- a/src/lua/utils.lua
+++ b/src/lua/utils.lua
@@ -1,3 +1,6 @@
+local ffi = require('ffi')
+local tarantool = require('tarantool')
+
 local utils = {}
 
 -- Same as type(), but returns 'number' if 'param' is
@@ -111,4 +114,34 @@ function utils.update_param_table(table, defaults)
     return new_table
 end
 
+ffi.cdef[[
+void
+__asan_poison_memory_region(void const volatile *addr, size_t size);
+void
+__asan_unpoison_memory_region(void const volatile *addr, size_t size);
+int
+__asan_address_is_poisoned(void const volatile *addr);
+]]
+
+if tarantool.build.asan then
+    utils.poison_memory_region = function(start, size)
+        ffi.C.__asan_poison_memory_region(start, size)
+    end
+    utils.unpoison_memory_region = function(start, size)
+        ffi.C.__asan_unpoison_memory_region(start, size)
+    end
+    utils.memory_region_is_poisoned = function(start, size)
+        for i = 0, size - 1 do
+            if ffi.C.__asan_address_is_poisoned(start + i) == 0 then
+                return false
+            end
+        end
+        return true
+    end
+else
+    utils.poison_memory_region = function() end
+    utils.unpoison_memory_region = function() end
+    utils.memory_region_is_poisoned = function() return false end
+end
+
 return utils
diff --git a/test/app-luatest/buffer_test.lua b/test/app-luatest/buffer_test.lua
new file mode 100644
index 0000000000..46a32e2ff0
--- /dev/null
+++ b/test/app-luatest/buffer_test.lua
@@ -0,0 +1,81 @@
+local tarantool = require('tarantool')
+local buffer = require('buffer')
+local utils = require('internal.utils')
+local ffi = require('ffi')
+local t = require('luatest')
+
+local g = t.group()
+
+g.test_poison = function()
+    t.skip_if(not tarantool.build.asan, 'only make sense with ASAN')
+
+    local buf = buffer.ibuf()
+    local is_poisoned = function(buf, ptr)
+        t.assert(utils.memory_region_is_poisoned(ptr, buf:unused()))
+    end
+
+    -- Test poison on allocation. --
+    local ptr = buf:alloc(99)
+    t.assert(ptr ~= nil)
+    ffi.fill(ptr, 99)
+    is_poisoned(buf, ptr + 99)
+
+    t.assert(buf:unused() > 133)
+    ptr = buf:alloc(133)
+    t.assert(ptr ~= nil)
+    ffi.fill(ptr, 133)
+    is_poisoned(buf, ptr + 133)
+
+    local size = buf:unused() + 77
+    ptr = buf:alloc(size)
+    t.assert(ptr ~= nil)
+    ffi.fill(ptr, size)
+    t.assert_gt(buf:unused(), 0)
+    is_poisoned(buf, ptr + size)
+
+    -- Test poison after reset. --
+    buf:reset()
+    ptr = buf:alloc(0)
+    is_poisoned(buf, ptr)
+
+    -- Test poison on reserve. --
+    ptr = buf:reserve(777)
+    t.assert(ptr ~= nil)
+    t.assert_ge(buf:unused(), 777)
+    ffi.fill(ptr, buf:unused())
+    ptr = buf:alloc(333)
+    t.assert(ptr ~= nil)
+    ffi.fill(ptr, 333)
+    is_poisoned(buf, ptr + 333)
+
+    t.assert_gt(buf:unused(), 888)
+    ptr = buf:reserve(333)
+    t.assert(ptr ~= nil)
+    t.assert_ge(buf:unused(), 333)
+    ffi.fill(ptr, buf:unused())
+    ptr = buf:alloc(888)
+    t.assert(ptr ~= nil)
+    ffi.fill(ptr, 888)
+    is_poisoned(buf, ptr + 888)
+
+    size = buf:unused() + 133;
+    ptr = buf:reserve(size)
+    t.assert(ptr ~= nil)
+    t.assert_ge(buf:unused(), size)
+    ffi.fill(ptr, buf:unused())
+    t.assert_gt(buf:unused(), 0)
+    ptr = buf:alloc(size)
+    t.assert(ptr ~= nil)
+    t.assert_gt(buf:unused(), 0)
+    is_poisoned(buf, ptr + size)
+
+    size = buf:unused(buf) + 221;
+    buf:read(337)
+    ptr = buf:reserve(size)
+    t.assert(ptr ~= nil)
+    t.assert_ge(buf:unused(), size)
+    ffi.fill(ptr, buf:unused())
+    ptr = buf:alloc(size)
+    t.assert_gt(buf:unused(), 0)
+    is_poisoned(buf, ptr + size)
+end
diff --git a/test/box-tap/merger.test.lua b/test/box-tap/merger.test.lua
index 2d313daa3a..f22aaa5c5b 100755
--- a/test/box-tap/merger.test.lua
+++ b/test/box-tap/merger.test.lua
@@ -44,10 +44,10 @@ local function truncated_msgpack_buffer(data, trunc)
     local len = data:len()
     local buf = buffer.ibuf()
     -- Ensure we have enough buffer to write len + trunc bytes.
-    buf:reserve(len + trunc)
-    local p = buf:alloc(len)
+    local p = buf:reserve(len + trunc)
     -- Ensure len bytes follows with trunc zero bytes.
     ffi.copy(p, data .. string.rep('\0', trunc), len + trunc)
+    buf:alloc(len)
     return buf
 end
 
-- 
GitLab