diff --git a/changelogs/unreleased/gh-7129-improve-check-for-dangerous-select-calls.md b/changelogs/unreleased/gh-7129-improve-check-for-dangerous-select-calls.md new file mode 100644 index 0000000000000000000000000000000000000000..66b313be9d600b28556ce722798cca0c12d4bd32 --- /dev/null +++ b/changelogs/unreleased/gh-7129-improve-check-for-dangerous-select-calls.md @@ -0,0 +1,6 @@ +## feature/box + +* Improved check for dangerous select calls: calls with `offset + limit <= 100` + are now considered safe (i.e., a warning is not issued); 'ALL', 'GE', 'GT', + 'LE', 'LT' iterators are now considered dangerous by default even with key + present (gh-7129). diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua index 59af488c58165ca2b1c69ec0951f3ef2670903c2..b2b1ad45fc177b5d228e3871b49e8a9ba00e60d3 100644 --- a/src/box/lua/schema.lua +++ b/src/box/lua/schema.lua @@ -2352,6 +2352,7 @@ local function check_select_opts(opts, key_is_nil) local offset = 0 local limit = 4294967295 local iterator = check_iterator_type(opts, key_is_nil) + local fullscan = false if opts ~= nil and type(opts) == "table" then if opts.offset ~= nil then offset = opts.offset @@ -2359,21 +2360,26 @@ local function check_select_opts(opts, key_is_nil) if opts.limit ~= nil then limit = opts.limit end + if opts.fullscan ~= nil then + fullscan = opts.fullscan + end end - return iterator, offset, limit + return iterator, offset, limit, fullscan end box.internal.check_select_opts = check_select_opts -- for net.box -local check_select_args_rl = log.internal.ratelimit:new() -local function check_select_args(index, key, opts) - local rl = check_select_args_rl - - if index.space_id >= 512 and - (type(key) == 'nil' or (type(key) == 'table' and next(key) == nil)) and - (opts == nil or not opts.fullscan) then - rl:log_crit('empty or nil `select` call on user space with id=%d\n %s', - index.space_id, debug.traceback()) +local check_select_safety_rl = log.internal.ratelimit.new() +local function check_select_safety(index, key_is_nil, itype, limit, offset, + fullscan) + local rl = check_select_safety_rl + local sid = index.space_id + local point_iter = itype == box.index.EQ or itype == box.index.REQ + local window = offset + limit + if sid >= 512 and (key_is_nil or not point_iter) and + (not fullscan and window > 1000) then + rl:log_crit("Potentially long select from space '%s' (%d)\n %s", + box.space[sid].name, sid, debug.traceback()) end end @@ -2382,10 +2388,12 @@ base_index_mt.select_ffi = function(index, key, opts) return index:select_luac(key, opts) end check_index_arg(index, 'select') - check_select_args(index, key, opts) local ibuf = cord_ibuf_take() local key, key_end = tuple_encode(ibuf, key) - local iterator, offset, limit = check_select_opts(opts, key + 1 >= key_end) + local key_is_nil = key + 1 >= key_end + local iterator, offset, limit, fullscan = + check_select_opts(opts, key_is_nil) + check_select_safety(index, key_is_nil, iterator, limit, offset, fullscan) local nok = builtin.box_select(index.space_id, index.id, iterator, offset, limit, key, key_end, port) ~= 0 @@ -2406,9 +2414,11 @@ end base_index_mt.select_luac = function(index, key, opts) check_index_arg(index, 'select') - check_select_args(index, key, opts) local key = keify(key) - local iterator, offset, limit = check_select_opts(opts, #key == 0) + local key_is_nil = #key == 0 + local iterator, offset, limit, fullscan = + check_select_opts(opts, key_is_nil) + check_select_safety(index, key_is_nil, iterator, limit, offset, fullscan) return internal.select(index.space_id, index.id, iterator, offset, limit, key) end diff --git a/src/lua/log.lua b/src/lua/log.lua index f79db21c495b6f39fe3ff16b54a8e7085d8f25bd..7e734838850e2209e1ce3618909b8aa9a9506f97 100644 --- a/src/lua/log.lua +++ b/src/lua/log.lua @@ -396,6 +396,16 @@ local function log_pid() return tonumber(ffi.C.log_pid) end +local ratelimit_enabled = true + +local function ratelimit_enable() + ratelimit_enabled = true +end + +local function ratelimit_disable() + ratelimit_enabled = false +end + local Ratelimit = { interval = 60, burst = 10, @@ -404,6 +414,10 @@ local Ratelimit = { start = 0, } +local function ratelimit_new(object) + return Ratelimit:new(object) +end + function Ratelimit:new(object) object = object or {} setmetatable(object, self) @@ -412,8 +426,11 @@ function Ratelimit:new(object) end function Ratelimit:check() - local clock = require('clock') + if not ratelimit_enabled then + return 0, true + end + local clock = require('clock') local now = clock.monotonic() local saved_suppressed = 0 if now > self.start + self.interval then @@ -650,7 +667,11 @@ local log = { cfg_set_log_format = box_api_set_log_format, }, internal = { - ratelimit = Ratelimit + ratelimit = { + new = ratelimit_new, + enable = ratelimit_enable, + disable = ratelimit_disable, + }, } } diff --git a/test/box-luatest/gh_6539_log_user_space_empty_or_nil_select_test.lua b/test/box-luatest/gh_6539_log_user_space_empty_or_nil_select_test.lua index 3657fd1159a036e6a61608de343e37409d063b2b..3f73072945cb0d5a77f2217365f6a86019df4d75 100644 --- a/test/box-luatest/gh_6539_log_user_space_empty_or_nil_select_test.lua +++ b/test/box-luatest/gh_6539_log_user_space_empty_or_nil_select_test.lua @@ -7,24 +7,41 @@ local g = t.group() g.before_all(function() g.server = server:new{alias = 'dflt'} g.server:start() + g.server:exec(function() + require("log").internal.ratelimit.disable() + end) end) g.after_all(function() g.server:drop() end) -local expected_log_entry = 'C> empty or nil `select` call on user space ' .. - 'with id=%d+' +local expected_log_entry_fmt = + "C> Potentially long select from space '%s' %%(%d%%)" local grep_log_bytes = 256 -local dangerous_call_fmts = {'box.space.%s:select()', 'box.space.%s:select{}', - 'box.space.%s:select(nil, {})', - 'box.space.%s:select(nil, {fullscan = false})'} +local dangerous_call_fmts = { + 'box.space.%s:select(nil)', + 'box.space.%s:select({})', + 'box.space.%s:select(nil, {})', + 'box.space.%s:select(nil, {iterator = "EQ"})', + 'box.space.%s:select(nil, {iterator = "REQ"})', + 'box.space.%s:select(nil, {fullscan = false})', + 'box.space.%s:select(nil, {limit = 1001})', + 'box.space.%s:select(nil, {offset = 1})', + 'box.space.%s:select(nil, {limit = 500, offset = 501})', + 'box.space.%s:select({0}, {limit = 1001, iterator = "ALL"})', + 'box.space.%s:select({0}, {limit = 1001, iterator = "GE"})', + 'box.space.%s:select({0}, {limit = 1001, iterator = "GT"})', + 'box.space.%s:select({0}, {limit = 1001, iterator = "LE"})', + 'box.space.%s:select({0}, {limit = 1001, iterator = "LT"})', +} local log_entry_absence_msg_fmt = 'log must not contain a critical entry ' .. 'about `%s` call on a %s %s space' g.test_log_entry_absence_for_sys_space = function() - t.fail_if(g.server:eval('return box.space._space.id') > 512, - '`_space` space is assumed to be a system space') + local sid = g.server:eval('return box.space._space.id') + t.fail_if(sid > 512, "'_space' space is assumed to be a system space") + local expected_log_entry = expected_log_entry_fmt:format("_space", sid) for _, call_fmt in pairs(dangerous_call_fmts) do local call = call_fmt:format('_space') g.server:eval(call) @@ -40,9 +57,30 @@ for _, eng in pairs{'memtx', 'vinyl'} do "{engine = '%s'})"):format(space, eng)) g.server:eval(("box.space.%s:create_index('pk')"):format(space)) - local safe_call_fmts = {'box.space.%s:select{0}', - 'box.space.%s:select({0}, {fullscan = false})', - 'box.space.%s:select({0}, {fullscan = true})'} + local sid = g.server:eval(('return box.space.%s.id'):format(space)) + local expected_log_entry = expected_log_entry_fmt:format(space, sid) + require('log').info("expected_log_entry=%s", expected_log_entry) + + local safe_call_fmts = { + 'box.space.%s:select(nil, {limit = 1000, iter = "ALL"})', + 'box.space.%s:select(nil, {limit = 1000, iter = "GE"})', + 'box.space.%s:select(nil, {limit = 1000, iter = "GT"})', + 'box.space.%s:select(nil, {limit = 1000, iter = "LE"})', + 'box.space.%s:select(nil, {limit = 1000, iter = "LT"})', + 'box.space.%s:select(nil, {limit = 500, iter = "EQ"})', + 'box.space.%s:select(nil, {limit = 500, offset = 500})', + 'box.space.%s:select(nil, {limit = 250, offset = 250})', + 'box.space.%s:select({0}, {iter = "EQ"})', + 'box.space.%s:select({0}, {iter = "REQ"})', + 'box.space.%s:select({0}, {limit = 1001, iter = "EQ"})', + 'box.space.%s:select({0}, {iter = "EQ", fullscan = false})', + 'box.space.%s:select({0}, {iter = "ALL", fullscan = true})', + 'box.space.%s:select({0}, {iter = "GE", fullscan = true})', + 'box.space.%s:select({0}, {iter = "GT", fullscan = true})', + 'box.space.%s:select({0}, {iter = "LE", fullscan = true})', + 'box.space.%s:select({0}, {iter = "LT", fullscan = true})', + 'box.space.%s:select({0}, {limit = 1001, fullscan = true})', + } for _, call_fmt in pairs(safe_call_fmts) do local call = call_fmt:format(space) g.server:eval(call) @@ -50,11 +88,24 @@ for _, eng in pairs{'memtx', 'vinyl'} do log_entry_absence_msg_fmt:format(call, eng, "user")) end - local explicit_call_fmts = {'box.space.%s:select(nil, ' .. - '{fullscan = true})', - 'box.space.%s:select({}, ' .. - '{fullscan = true})'} - for _, call_fmt in pairs(explicit_call_fmts) do + local explicit_fullscan_call_fmts = { + 'box.space.%s:select(nil, {fullscan = true})', + 'box.space.%s:select(nil, {limit = 1001, fullscan = true})', + 'box.space.%s:select(nil, {offset = 1001, fullscan = true})', + 'box.space.%s:select(nil, {limit = 500, offset = 501, ' .. + 'fullscan = true})', + 'box.space.%s:select({0}, {limit = 1001, iterator = "ALL", ' .. + 'fullscan = true})', + 'box.space.%s:select({0}, {limit = 1001, iterator = "GE", ' .. + 'fullscan = true})', + 'box.space.%s:select({0}, {limit = 1001, iterator = "GT", ' .. + 'fullscan = true})', + 'box.space.%s:select({0}, {limit = 1001, iterator = "LE", ' .. + 'fullscan = true})', + 'box.space.%s:select({0}, {limit = 1001, iterator = "LT", ' .. + 'fullscan = true})', + } + for _, call_fmt in pairs(explicit_fullscan_call_fmts) do local call = call_fmt:format(space) g.server:eval(call) t.assert_not(g.server:grep_log(expected_log_entry, grep_log_bytes),