[PATCH v1 2/3] schema: extend _func space to persist lua functions
Kirill Shcherbatov
kshcherbatov at tarantool.org
Tue May 14 16:29:21 MSK 2019
This patch extends _func system space with a new string field
lua_code to persist Lua functions.
Refactored func hash machinery to pass a new function object and
to return previous one. This allows to implement fault-tolerant
func_new call that constructs a lua function object.
Each persistent function may use limited amount of Lua functions
and modules:
-assert -error -pairs -ipairs -next -pcall -xpcall -type
-print -select -string -tonumber -tostring -unpack -math -utf8
Global variables are forbidden in persistent Lua functions.
This patch makes persistent Lua functions are available via
net.box.connect() :call method.
Example:
lua_code = [[function(a, b) return a + b end]]
box.schema.func.create('sum', {lua_code = lua_code})
conn:call("sum", {1, 3})
Part of #4182
Needed for #1260
---
src/box/alter.cc | 85 +++++++++++-----
src/box/bootstrap.snap | Bin 4374 -> 4393 bytes
src/box/call.c | 2 +-
src/box/func.c | 109 ++++++++++++++++++--
src/box/func.h | 15 ++-
src/box/func_def.h | 10 +-
src/box/lua/call.c | 40 +++++++-
src/box/lua/call.h | 4 +-
src/box/lua/schema.lua | 7 +-
src/box/lua/upgrade.lua | 15 +++
src/box/schema.cc | 46 ++++-----
src/box/schema.h | 15 +--
src/box/schema_def.h | 1 +
test/box-py/bootstrap.result | 6 +-
test/box/access_misc.result | 4 +-
test/box/persistent_func.result | 163 ++++++++++++++++++++++++++++++
test/box/persistent_func.test.lua | 78 ++++++++++++++
17 files changed, 517 insertions(+), 83 deletions(-)
create mode 100644 test/box/persistent_func.result
create mode 100644 test/box/persistent_func.test.lua
diff --git a/src/box/alter.cc b/src/box/alter.cc
index 9279426d2..0d23dbf0e 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -2402,21 +2402,41 @@ func_def_get_ids_from_tuple(struct tuple *tuple, uint32_t *fid, uint32_t *uid)
static struct func_def *
func_def_new_from_tuple(struct tuple *tuple)
{
- uint32_t len;
- const char *name = tuple_field_str_xc(tuple, BOX_FUNC_FIELD_NAME,
- &len);
- if (len > BOX_NAME_MAX)
+ uint32_t name_len, lua_code_len;
+ const char *name, *lua_code;
+ name = tuple_field_str_xc(tuple, BOX_FUNC_FIELD_NAME, &name_len);
+ if (name_len > BOX_NAME_MAX) {
tnt_raise(ClientError, ER_CREATE_FUNCTION,
tt_cstr(name, BOX_INVALID_NAME_MAX),
"function name is too long");
- identifier_check_xc(name, len);
- struct func_def *def = (struct func_def *) malloc(func_def_sizeof(len));
+ }
+ identifier_check_xc(name, name_len);
+ if (tuple_field_count(tuple) > BOX_FUNC_FIELD_LUA_CODE) {
+ lua_code = tuple_field_str_xc(tuple, BOX_FUNC_FIELD_LUA_CODE,
+ &lua_code_len);
+ } else {
+ lua_code = NULL;
+ lua_code_len = 0;
+ }
+
+ uint32_t def_sz = func_def_sizeof(name_len, lua_code_len);
+ struct func_def *def = (struct func_def *) malloc(def_sz);
if (def == NULL)
- tnt_raise(OutOfMemory, func_def_sizeof(len), "malloc", "def");
+ tnt_raise(OutOfMemory, def_sz, "malloc", "def");
auto def_guard = make_scoped_guard([=] { free(def); });
func_def_get_ids_from_tuple(tuple, &def->fid, &def->uid);
- memcpy(def->name, name, len);
- def->name[len] = 0;
+
+ def->name = (char *)def + sizeof(struct func_def);
+ memcpy(def->name, name, name_len);
+ def->name[name_len] = 0;
+ if (lua_code_len > 0) {
+ def->lua_code = def->name + name_len + 1;
+ memcpy(def->lua_code, lua_code, lua_code_len);
+ def->lua_code[lua_code_len] = 0;
+ } else {
+ def->lua_code = NULL;
+ }
+
if (tuple_field_count(tuple) > BOX_FUNC_FIELD_SETUID)
def->setuid = tuple_field_u32_xc(tuple, BOX_FUNC_FIELD_SETUID);
else
@@ -2433,30 +2453,32 @@ func_def_new_from_tuple(struct tuple *tuple)
/* Lua is the default. */
def->language = FUNC_LANGUAGE_LUA;
}
+ if (def->language != FUNC_LANGUAGE_LUA && lua_code_len > 0) {
+ tnt_raise(ClientError, ER_CREATE_FUNCTION, name,
+ "function lua_code may be specified only for "
+ "Lua language");
+ }
def_guard.is_active = false;
return def;
}
/** Remove a function from function cache */
static void
-func_cache_remove_func(struct trigger * /* trigger */, void *event)
+func_cache_remove_func(struct trigger *trigger, void * /* event */)
{
- struct txn_stmt *stmt = txn_last_stmt((struct txn *) event);
- uint32_t fid = tuple_field_u32_xc(stmt->old_tuple ?
- stmt->old_tuple : stmt->new_tuple,
- BOX_FUNC_FIELD_ID);
- func_cache_delete(fid);
+ struct func *old_func = (struct func *) trigger->data;
+ func_cache_delete(old_func->def->fid);
+ func_delete(old_func);
}
/** Replace a function in the function cache */
static void
-func_cache_replace_func(struct trigger * /* trigger */, void *event)
+func_cache_replace_func(struct trigger *trigger, void * /* event */)
{
- struct txn_stmt *stmt = txn_last_stmt((struct txn*) event);
- struct func_def *def = func_def_new_from_tuple(stmt->new_tuple);
- auto def_guard = make_scoped_guard([=] { free(def); });
- func_cache_replace(def);
- def_guard.is_active = false;
+ struct func *new_func = (struct func *) trigger->data;
+ struct func *old_func;
+ func_cache_replace(new_func, &old_func);
+ func_delete(old_func);
}
/**
@@ -2477,13 +2499,20 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
struct func *old_func = func_by_id(fid);
if (new_tuple != NULL && old_func == NULL) { /* INSERT */
struct func_def *def = func_def_new_from_tuple(new_tuple);
+ auto def_guard = make_scoped_guard([=] { free(def); });
access_check_ddl(def->name, def->fid, def->uid, SC_FUNCTION,
PRIV_C);
- auto def_guard = make_scoped_guard([=] { free(def); });
- func_cache_replace(def);
+ struct func *func = func_new(def);
+ if (func == NULL)
+ diag_raise();
+ auto func_guard = make_scoped_guard([=] { func_delete(func); });
def_guard.is_active = false;
+ struct func *old_func = NULL;
+ func_cache_replace(func, &old_func);
+ assert(old_func == NULL);
+ func_guard.is_active = false;
struct trigger *on_rollback =
- txn_alter_trigger_new(func_cache_remove_func, NULL);
+ txn_alter_trigger_new(func_cache_remove_func, func);
txn_on_rollback(txn, on_rollback);
} else if (new_tuple == NULL) { /* DELETE */
uint32_t uid;
@@ -2501,15 +2530,19 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
"function has grants");
}
struct trigger *on_commit =
- txn_alter_trigger_new(func_cache_remove_func, NULL);
+ txn_alter_trigger_new(func_cache_remove_func, old_func);
txn_on_commit(txn, on_commit);
} else { /* UPDATE, REPLACE */
struct func_def *def = func_def_new_from_tuple(new_tuple);
auto def_guard = make_scoped_guard([=] { free(def); });
access_check_ddl(def->name, def->fid, def->uid, SC_FUNCTION,
PRIV_A);
+ struct func *func = func_new(def);
+ if (func == NULL)
+ diag_raise();
+ def_guard.is_active = false;
struct trigger *on_commit =
- txn_alter_trigger_new(func_cache_replace_func, NULL);
+ txn_alter_trigger_new(func_cache_replace_func, func);
txn_on_commit(txn, on_commit);
}
}
diff --git a/src/box/call.c b/src/box/call.c
index 56da53fb3..773b914b1 100644
--- a/src/box/call.c
+++ b/src/box/call.c
@@ -202,7 +202,7 @@ box_process_call(struct call_request *request, struct port *port)
if (func && func->def->language == FUNC_LANGUAGE_C) {
rc = box_c_call(func, request, port);
} else {
- rc = box_lua_call(request, port);
+ rc = box_lua_call(func, request, port);
}
/* Restore the original user */
if (orig_credentials)
diff --git a/src/box/func.c b/src/box/func.c
index a817851fd..b68c089ad 100644
--- a/src/box/func.c
+++ b/src/box/func.c
@@ -29,11 +29,13 @@
* SUCH DAMAGE.
*/
#include "func.h"
+#include "box/lua/util.h"
#include "trivia/config.h"
#include "assoc.h"
#include "lua/utils.h"
#include "error.h"
#include "diag.h"
+#include "fiber.h"
#include <dlfcn.h>
/**
@@ -355,6 +357,88 @@ restore:
return -1;
}
+/**
+ * Assemble a Lua function object on Lua stack and return
+ * the reference.
+ * Returns func object reference on success, -1 otherwise.
+ */
+static int
+func_lua_code_load(struct func_def *def)
+{
+ int rc = -1;
+ struct region *region = &fiber()->gc;
+ size_t region_svp = region_used(region);
+ struct lua_State *L = lua_newthread(tarantool_L);
+ int coro_ref = luaL_ref(tarantool_L, LUA_REGISTRYINDEX);
+
+ const char *load_pref = "return ";
+ uint32_t load_str_sz =
+ strlen(load_pref) + strlen(def->lua_code) + 1;
+ char *load_str = region_alloc(region, load_str_sz);
+ if (load_str == NULL) {
+ diag_set(OutOfMemory, load_str_sz, "region",
+ "load_str");
+ goto end;
+ }
+ sprintf(load_str, "%s%s", load_pref, def->lua_code);
+ if (luaL_loadstring(L, load_str) != 0 ||
+ lua_pcall(L, 0, 1, 0) != 0 || !lua_isfunction(L, -1)) {
+ diag_set(ClientError, ER_LOAD_FUNCTION, def->name,
+ def->lua_code);
+ goto end;
+ }
+ /*
+ * Allow assembled function to use only a limited number
+ * of functions and modules. All modules are exported
+ * via deepcopy to prevent the user from overriding
+ * the system context.
+ *
+ * The debug.getinfo() inspection for upvalues is
+ * redundant, because no one can create function that
+ * doesn't return function and that has such env_exports
+ * restrictions.
+ */
+ int count;
+ const char *deepcopy = "table.deepcopy";
+ if (luaT_func_find(L, deepcopy, deepcopy + strlen(deepcopy),
+ &count) != 0)
+ goto end;
+ int deepcopy_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+ struct {
+ const char *name;
+ bool deep_copy;
+ } env_exports[] = {
+ {"assert", false}, {"error", false}, {"ipairs", false},
+ {"math", true}, {"next", false}, {"pairs", false},
+ {"pcall", false}, {"print", false}, {"select", false},
+ {"string", true}, {"table", true}, {"tonumber", false},
+ {"tostring", false}, {"type", false}, {"unpack", false},
+ {"xpcall", false}, {"utf8", false}
+ };
+ lua_createtable(L, nelem(env_exports), 0);
+ for (unsigned i = 0; i < nelem(env_exports); i++) {
+ uint32_t name_len = strlen(env_exports[i].name);
+ if (env_exports[i].deep_copy)
+ lua_rawgeti(L, LUA_REGISTRYINDEX, deepcopy_ref);
+ if (luaT_func_find(L, env_exports[i].name,
+ env_exports[i].name + name_len,
+ &count) != 0) {
+ luaL_unref(L, LUA_REGISTRYINDEX, deepcopy_ref);
+ goto end;
+ }
+ if (env_exports[i].deep_copy)
+ lua_call(L, 1, LUA_MULTRET);
+ lua_setfield(L, -2, env_exports[i].name);
+ }
+ lua_setfenv(L, -2);
+ luaL_unref(L, LUA_REGISTRYINDEX, deepcopy_ref);
+ rc = luaL_ref(L, LUA_REGISTRYINDEX);
+end:
+ region_truncate(region, region_svp);
+ luaL_unref(L, LUA_REGISTRYINDEX, coro_ref);
+ return rc;
+}
+
struct func *
func_new(struct func_def *def)
{
@@ -380,6 +464,15 @@ func_new(struct func_def *def)
func->owner_credentials.auth_token = BOX_USER_MAX; /* invalid value */
func->func = NULL;
func->module = NULL;
+ if (func->def->lua_code != NULL) {
+ func->lua_func_ref = func_lua_code_load(def);
+ if (func->lua_func_ref == -1) {
+ free(func);
+ return NULL;
+ }
+ } else {
+ func->lua_func_ref = -1;
+ }
return func;
}
@@ -395,8 +488,11 @@ func_unload(struct func *func)
}
module_gc(func->module);
}
+ if (func->lua_func_ref != -1)
+ luaL_unref(tarantool_L, LUA_REGISTRYINDEX, func->lua_func_ref);
func->module = NULL;
func->func = NULL;
+ func->lua_func_ref = -1;
}
/**
@@ -452,17 +548,18 @@ func_call(struct func *func, box_function_ctx_t *ctx, const char *args,
}
void
-func_update(struct func *func, struct func_def *def)
+func_delete(struct func *func)
{
func_unload(func);
free(func->def);
- func->def = def;
+ free(func);
}
void
-func_delete(struct func *func)
+func_capture_module(struct func *new_func, struct func *old_func)
{
- func_unload(func);
- free(func->def);
- free(func);
+ new_func->module = old_func->module;
+ new_func->func = old_func->func;
+ old_func->module = NULL;
+ old_func->func = NULL;
}
diff --git a/src/box/func.h b/src/box/func.h
index 8dcd61d7b..b00ed5690 100644
--- a/src/box/func.h
+++ b/src/box/func.h
@@ -65,6 +65,11 @@ struct func {
* Anchor for module membership.
*/
struct rlist item;
+ /**
+ * The reference index of Lua function object.
+ * Is equal to -1 when undefined.
+ */
+ int lua_func_ref;
/**
* For C functions, the body of the function.
*/
@@ -100,9 +105,6 @@ module_free(void);
struct func *
func_new(struct func_def *def);
-void
-func_update(struct func *func, struct func_def *def);
-
void
func_delete(struct func *func);
@@ -113,6 +115,13 @@ int
func_call(struct func *func, box_function_ctx_t *ctx, const char *args,
const char *args_end);
+/**
+ * Take over the modules that belonged to the old function.
+ * Reset module and function pointers in onl function.
+*/
+void
+func_capture_module(struct func *new_func, struct func *old_func);
+
/**
* Reload dynamically loadable module.
*
diff --git a/src/box/func_def.h b/src/box/func_def.h
index 5b52ab498..19a4df959 100644
--- a/src/box/func_def.h
+++ b/src/box/func_def.h
@@ -54,6 +54,10 @@ struct func_def {
uint32_t fid;
/** Owner of the function. */
uint32_t uid;
+ /** Function name. */
+ char *name;
+ /** The Lua function code. */
+ char *lua_code;
/**
* True if the function requires change of user id before
* invocation.
@@ -63,8 +67,6 @@ struct func_def {
* The language of the stored function.
*/
enum func_language language;
- /** Function name. */
- char name[0];
};
/**
@@ -73,10 +75,10 @@ struct func_def {
* for a function of length @a a name_len.
*/
static inline size_t
-func_def_sizeof(uint32_t name_len)
+func_def_sizeof(uint32_t name_len, uint32_t lua_code_len)
{
/* +1 for '\0' name terminating. */
- return sizeof(struct func_def) + name_len + 1;
+ return sizeof(struct func_def) + name_len + 1 + lua_code_len + 1;
}
/**
diff --git a/src/box/lua/call.c b/src/box/lua/call.c
index 1a0fb4df0..496c00e83 100644
--- a/src/box/lua/call.c
+++ b/src/box/lua/call.c
@@ -32,6 +32,8 @@
#include "box/call.h"
#include "box/error.h"
#include "fiber.h"
+#include "box/func.h"
+#include "box/schema.h"
#include "lua/utils.h"
#include "lua/msgpack.h"
@@ -250,6 +252,37 @@ execute_lua_call(lua_State *L)
return lua_gettop(L);
}
+/**
+ * Execute the persistent Lua function.
+ * Assemble Lua object when required.
+ */
+static int
+execute_lua_ref_call(lua_State *L)
+{
+ struct call_request *request = (struct call_request *)
+ lua_topointer(L, 1);
+ lua_settop(L, 0);
+
+ const char *name = request->name;
+ uint32_t name_len = mp_decode_strl(&name);
+
+ struct func *func = func_by_name(name, name_len);
+ assert(func != NULL);
+ /* Construct Lua function if required. */
+ assert(func->lua_func_ref != -1);
+ lua_rawgeti(L, LUA_REGISTRYINDEX, func->lua_func_ref);
+
+ /* Push the rest of args (a tuple). */
+ const char *args = request->args;
+ uint32_t arg_count = mp_decode_array(&args);
+ luaL_checkstack(L, arg_count, "lua_func: out of stack");
+ for (uint32_t i = 0; i < arg_count; i++)
+ luamp_decode(L, luaL_msgpack_default, &args);
+
+ lua_call(L, arg_count, LUA_MULTRET);
+ return lua_gettop(L);
+}
+
static int
execute_lua_eval(lua_State *L)
{
@@ -393,9 +426,12 @@ box_process_lua(struct call_request *request, struct port *base,
}
int
-box_lua_call(struct call_request *request, struct port *port)
+box_lua_call(struct func *func, struct call_request *request, struct port *port)
{
- return box_process_lua(request, port, execute_lua_call);
+ if (func != NULL && func->def->lua_code != NULL)
+ return box_process_lua(request, port, execute_lua_ref_call);
+ else
+ return box_process_lua(request, port, execute_lua_call);
}
int
diff --git a/src/box/lua/call.h b/src/box/lua/call.h
index 0542123da..d8ef65900 100644
--- a/src/box/lua/call.h
+++ b/src/box/lua/call.h
@@ -42,13 +42,15 @@ box_lua_call_init(struct lua_State *L);
struct port;
struct call_request;
+struct func;
/**
* Invoke a Lua stored procedure from the binary protocol
* (implementation of 'CALL' command code).
*/
int
-box_lua_call(struct call_request *request, struct port *port);
+box_lua_call(struct func *func, struct call_request *request,
+ struct port *port);
int
box_lua_eval(struct call_request *request, struct port *port);
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index f31cf7f2c..ca7da6501 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -1981,7 +1981,8 @@ box.schema.func.create = function(name, opts)
opts = opts or {}
check_param_table(opts, { setuid = 'boolean',
if_not_exists = 'boolean',
- language = 'string'})
+ language = 'string',
+ lua_code = 'string' })
local _func = box.space[box.schema.FUNC_ID]
local _vfunc = box.space[box.schema.VFUNC_ID]
local func = _vfunc.index.name:get{name}
@@ -1991,10 +1992,10 @@ box.schema.func.create = function(name, opts)
end
return
end
- opts = update_param_table(opts, { setuid = false, language = 'lua'})
+ opts = update_param_table(opts, { setuid = false, language = 'lua', lua_code = ''})
opts.language = string.upper(opts.language)
opts.setuid = opts.setuid and 1 or 0
- _func:auto_increment{session.euid(), name, opts.setuid, opts.language}
+ _func:auto_increment{session.euid(), name, opts.setuid, opts.language, opts.lua_code}
end
box.schema.func.drop = function(name, opts)
diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
index 89d6e3d52..c5296fc69 100644
--- a/src/box/lua/upgrade.lua
+++ b/src/box/lua/upgrade.lua
@@ -737,6 +737,20 @@ local function upgrade_to_2_1_3()
end
end
+local function upgrade_to_2_2_0()
+ local _func = box.space[box.schema.FUNC_ID]
+ local format = {}
+ format[1] = {name='id', type='unsigned'}
+ format[2] = {name='owner', type='unsigned'}
+ format[3] = {name='name', type='string'}
+ format[4] = {name='setuid', type='unsigned'}
+ format[5] = {name='lua_code', type='string'}
+ for _, v in box.space._func:pairs() do
+ box.space._func:replace(v:update{{'=', #v + 1, ''}})
+ end
+ _func:format(format)
+end
+
local function get_version()
local version = box.space._schema:get{'version'}
if version == nil then
@@ -768,6 +782,7 @@ local function upgrade(options)
{version = mkversion(2, 1, 1), func = upgrade_to_2_1_1, auto = true},
{version = mkversion(2, 1, 2), func = upgrade_to_2_1_2, auto = true},
{version = mkversion(2, 1, 3), func = upgrade_to_2_1_3, auto = true},
+ {version = mkversion(2, 2, 0), func = upgrade_to_2_2_0, auto = true},
}
for _, handler in ipairs(handlers) do
diff --git a/src/box/schema.cc b/src/box/schema.cc
index 9a55c2f14..aae92874c 100644
--- a/src/box/schema.cc
+++ b/src/box/schema.cc
@@ -533,40 +533,35 @@ schema_free(void)
}
void
-func_cache_replace(struct func_def *def)
+func_cache_replace(struct func *new_func, struct func **old_func)
{
- struct func *old = func_by_id(def->fid);
- if (old) {
- func_update(old, def);
- return;
- }
if (mh_size(funcs) >= BOX_FUNCTION_MAX)
tnt_raise(ClientError, ER_FUNCTION_MAX, BOX_FUNCTION_MAX);
- struct func *func = func_new(def);
- if (func == NULL) {
-error:
- panic_syserror("Out of memory for the data "
- "dictionary cache (stored function).");
- }
- const struct mh_i32ptr_node_t node = { def->fid, func };
- mh_int_t k1 = mh_i32ptr_put(funcs, &node, NULL, NULL);
- if (k1 == mh_end(funcs)) {
- func->def = NULL;
- func_delete(func);
- goto error;
- }
- size_t def_name_len = strlen(func->def->name);
- uint32_t name_hash = mh_strn_hash(func->def->name, def_name_len);
+
+ mh_int_t k1, k2;
+ struct mh_i32ptr_node_t *old_node = NULL;
+ const struct mh_i32ptr_node_t node = { new_func->def->fid, new_func };
+ size_t def_name_len = strlen(new_func->def->name);
+ uint32_t name_hash = mh_strn_hash(new_func->def->name, def_name_len);
const struct mh_strnptr_node_t strnode = {
- func->def->name, def_name_len, name_hash, func };
+ new_func->def->name, def_name_len, name_hash, new_func };
- mh_int_t k2 = mh_strnptr_put(funcs_by_name, &strnode, NULL, NULL);
+ k1 = mh_i32ptr_put(funcs, &node, &old_node, NULL);
+ if (k1 == mh_end(funcs))
+ goto error;
+ if (old_node != NULL)
+ *old_func = (struct func *)old_node->val;
+ k2 = mh_strnptr_put(funcs_by_name, &strnode, NULL, NULL);
if (k2 == mh_end(funcs_by_name)) {
mh_i32ptr_del(funcs, k1, NULL);
- func->def = NULL;
- func_delete(func);
goto error;
}
+ if (*old_func != NULL)
+ func_capture_module(new_func, *old_func);
+ return;
+error:
+ panic_syserror("Out of memory for the data dictionary cache "
+ "(stored function).");
}
void
@@ -582,7 +577,6 @@ func_cache_delete(uint32_t fid)
strlen(func->def->name));
if (k != mh_end(funcs))
mh_strnptr_del(funcs_by_name, k, NULL);
- func_delete(func);
}
struct func *
diff --git a/src/box/schema.h b/src/box/schema.h
index 6f9a96117..bc15c8a2c 100644
--- a/src/box/schema.h
+++ b/src/box/schema.h
@@ -163,14 +163,17 @@ schema_free();
struct space *schema_space(uint32_t id);
/**
- * Insert a new function or update the old one.
- *
- * @param def Function definition. In a case of success the ownership
- * of @a def is transfered to the data dictionary, thus the caller
- * must not delete it.
+ * Replace an existent (if any) function or insert a new one
+ * in function cache.
+ * @param func_new Function object to insert. In case of success,
+ * when old function object is exists, all loaded
+ * modules data are inherent with
+ * func_capture_module.
+ * @param func_old[out] The replaced function object if any.
+ * @retval Returns 0 on success, -1 otherwise.
*/
void
-func_cache_replace(struct func_def *def);
+func_cache_replace(struct func *new_func, struct func **old_func);
void
func_cache_delete(uint32_t fid);
diff --git a/src/box/schema_def.h b/src/box/schema_def.h
index eeeeb950b..31d2a594a 100644
--- a/src/box/schema_def.h
+++ b/src/box/schema_def.h
@@ -163,6 +163,7 @@ enum {
BOX_FUNC_FIELD_NAME = 2,
BOX_FUNC_FIELD_SETUID = 3,
BOX_FUNC_FIELD_LANGUAGE = 4,
+ BOX_FUNC_FIELD_LUA_CODE = 5,
};
/** _collation fields. */
diff --git a/test/box-py/bootstrap.result b/test/box-py/bootstrap.result
index 379f6c51f..ea8ae0984 100644
--- a/test/box-py/bootstrap.result
+++ b/test/box-py/bootstrap.result
@@ -4,7 +4,7 @@ box.internal.bootstrap()
box.space._schema:select{}
---
- - ['max_id', 511]
- - ['version', 2, 1, 3]
+ - ['version', 2, 2, 0]
...
box.space._cluster:select{}
---
@@ -49,7 +49,7 @@ box.space._space:select{}
'type': 'string'}, {'name': 'opts', 'type': 'map'}, {'name': 'parts', 'type': 'array'}]]
- [296, 1, '_func', 'memtx', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'owner',
'type': 'unsigned'}, {'name': 'name', 'type': 'string'}, {'name': 'setuid',
- 'type': 'unsigned'}]]
+ 'type': 'unsigned'}, {'name': 'lua_code', 'type': 'string'}]]
- [297, 1, '_vfunc', 'sysview', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'owner',
'type': 'unsigned'}, {'name': 'name', 'type': 'string'}, {'name': 'setuid',
'type': 'unsigned'}]]
@@ -141,7 +141,7 @@ box.space._user:select{}
...
box.space._func:select{}
---
-- - [1, 1, 'box.schema.user.info', 1, 'LUA']
+- - [1, 1, 'box.schema.user.info', 1, 'LUA', '']
...
box.space._priv:select{}
---
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index 36ebfae09..e1dd9b1cb 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -789,7 +789,7 @@ box.space._space:select()
'type': 'string'}, {'name': 'opts', 'type': 'map'}, {'name': 'parts', 'type': 'array'}]]
- [296, 1, '_func', 'memtx', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'owner',
'type': 'unsigned'}, {'name': 'name', 'type': 'string'}, {'name': 'setuid',
- 'type': 'unsigned'}]]
+ 'type': 'unsigned'}, {'name': 'lua_code', 'type': 'string'}]]
- [297, 1, '_vfunc', 'sysview', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'owner',
'type': 'unsigned'}, {'name': 'name', 'type': 'string'}, {'name': 'setuid',
'type': 'unsigned'}]]
@@ -821,7 +821,7 @@ box.space._space:select()
...
box.space._func:select()
---
-- - [1, 1, 'box.schema.user.info', 1, 'LUA']
+- - [1, 1, 'box.schema.user.info', 1, 'LUA', '']
...
session = nil
---
diff --git a/test/box/persistent_func.result b/test/box/persistent_func.result
new file mode 100644
index 000000000..ba544394e
--- /dev/null
+++ b/test/box/persistent_func.result
@@ -0,0 +1,163 @@
+env = require('test_run')
+---
+...
+test_run = env.new()
+---
+...
+--
+-- gh-4182: Add persistent LUA functions.
+--
+box.schema.user.grant('guest', 'execute', 'universe')
+---
+...
+net = require('net.box')
+---
+...
+conn = net.connect(box.cfg.listen)
+---
+...
+-- Test valid function.
+test_run:cmd("setopt delimiter ';'")
+---
+- true
+...
+lua_code = [[function(tuple)
+ if type(tuple.address) ~= 'string' then return nil, 'Invalid field type' end
+ local t = tuple.address:lower():split()
+ for k,v in pairs(t) do t[k] = {v} end
+ return t
+end
+]]
+test_run:cmd("setopt delimiter ''");
+---
+...
+box.schema.func.create('test', {lua_code = lua_code, language = "C"})
+---
+- error: 'Failed to create function ''test'': function lua_code may be specified only
+ for Lua language'
+...
+box.schema.func.create('test', {lua_code = lua_code})
+---
+...
+box.schema.func.exists('test')
+---
+- true
+...
+conn:call("test", {{address = "Moscow Dolgoprudny"}})
+---
+- [['moscow'], ['dolgoprudny']]
+...
+-- Test that monkey-patch attack is not possible.
+test_run:cmd("setopt delimiter ';'")
+---
+- true
+...
+lua_code_monkey = [[function(tuple)
+ math.abs = math.log
+ return tuple
+end
+]]
+test_run:cmd("setopt delimiter ''");
+---
+...
+box.schema.func.create('lua_code_monkey', {lua_code = lua_code_monkey})
+---
+...
+conn:call("lua_code_monkey", {{address = "Moscow Dolgoprudny"}})
+---
+- {'address': 'Moscow Dolgoprudny'}
+...
+math.abs(-666.666)
+---
+- 666.666
+...
+-- Test taht 'require' is forbidden.
+test_run:cmd("setopt delimiter ';'")
+---
+- true
+...
+lua_code_bad1 = [[function(tuple)
+ local json = require('json')
+ return json.encode(tuple)
+end
+]]
+test_run:cmd("setopt delimiter ''");
+---
+...
+box.schema.func.create('json_serializer', {lua_code = lua_code_bad1})
+---
+...
+conn:call("json_serializer", {{address = "Moscow Dolgoprudny"}})
+---
+- error: '[string "return function(tuple) ..."]:1: attempt to call global ''require''
+ (a nil value)'
+...
+-- Test function with spell error - case 1.
+test_run:cmd("setopt delimiter ';'")
+---
+- true
+...
+lua_code_bad2 = [[function(tuple)
+ ret tuple
+end
+]]
+test_run:cmd("setopt delimiter ''");
+---
+...
+box.schema.func.create('lua_code_bad2', {lua_code = lua_code_bad2})
+---
+- error: "Failed to dynamically load function 'lua_code_bad2': function(tuple) \tret
+ tuple end "
+...
+-- Test function with spell error - case 2.
+test_run:cmd("setopt delimiter ';'")
+---
+- true
+...
+lua_code_bad3 = [[func(tuple)
+ return tuple
+end
+]]
+test_run:cmd("setopt delimiter ''");
+---
+...
+box.schema.func.create('lua_code_bad3', {lua_code = lua_code_bad3})
+---
+- error: "Failed to dynamically load function 'lua_code_bad3': func(tuple) \treturn
+ tuple end "
+...
+conn:call("lua_code_bad3", {{address = "Moscow Dolgoprudny"}})
+---
+- error: Procedure 'lua_code_bad3' is not defined
+...
+conn:close()
+---
+...
+-- Restart server.
+test_run:cmd("restart server default")
+net = require('net.box')
+---
+...
+test_run = require('test_run').new()
+---
+...
+conn = net.connect(box.cfg.listen)
+---
+...
+conn:call("test", {{address = "Moscow Dolgoprudny"}})
+---
+- [['moscow'], ['dolgoprudny']]
+...
+conn:close()
+---
+...
+box.schema.func.drop('test')
+---
+...
+box.schema.func.exists('test')
+---
+- false
+...
+box.schema.user.revoke('guest', 'execute', 'universe')
+---
+...
diff --git a/test/box/persistent_func.test.lua b/test/box/persistent_func.test.lua
new file mode 100644
index 000000000..67a315197
--- /dev/null
+++ b/test/box/persistent_func.test.lua
@@ -0,0 +1,78 @@
+env = require('test_run')
+test_run = env.new()
+
+--
+-- gh-4182: Add persistent LUA functions.
+--
+box.schema.user.grant('guest', 'execute', 'universe')
+net = require('net.box')
+conn = net.connect(box.cfg.listen)
+
+-- Test valid function.
+test_run:cmd("setopt delimiter ';'")
+lua_code = [[function(tuple)
+ if type(tuple.address) ~= 'string' then return nil, 'Invalid field type' end
+ local t = tuple.address:lower():split()
+ for k,v in pairs(t) do t[k] = {v} end
+ return t
+end
+]]
+test_run:cmd("setopt delimiter ''");
+box.schema.func.create('test', {lua_code = lua_code, language = "C"})
+box.schema.func.create('test', {lua_code = lua_code})
+box.schema.func.exists('test')
+conn:call("test", {{address = "Moscow Dolgoprudny"}})
+
+-- Test that monkey-patch attack is not possible.
+test_run:cmd("setopt delimiter ';'")
+lua_code_monkey = [[function(tuple)
+ math.abs = math.log
+ return tuple
+end
+]]
+test_run:cmd("setopt delimiter ''");
+box.schema.func.create('lua_code_monkey', {lua_code = lua_code_monkey})
+conn:call("lua_code_monkey", {{address = "Moscow Dolgoprudny"}})
+math.abs(-666.666)
+
+-- Test taht 'require' is forbidden.
+test_run:cmd("setopt delimiter ';'")
+lua_code_bad1 = [[function(tuple)
+ local json = require('json')
+ return json.encode(tuple)
+end
+]]
+test_run:cmd("setopt delimiter ''");
+box.schema.func.create('json_serializer', {lua_code = lua_code_bad1})
+conn:call("json_serializer", {{address = "Moscow Dolgoprudny"}})
+
+-- Test function with spell error - case 1.
+test_run:cmd("setopt delimiter ';'")
+lua_code_bad2 = [[function(tuple)
+ ret tuple
+end
+]]
+test_run:cmd("setopt delimiter ''");
+box.schema.func.create('lua_code_bad2', {lua_code = lua_code_bad2})
+
+-- Test function with spell error - case 2.
+test_run:cmd("setopt delimiter ';'")
+lua_code_bad3 = [[func(tuple)
+ return tuple
+end
+]]
+test_run:cmd("setopt delimiter ''");
+box.schema.func.create('lua_code_bad3', {lua_code = lua_code_bad3})
+conn:call("lua_code_bad3", {{address = "Moscow Dolgoprudny"}})
+
+conn:close()
+-- Restart server.
+test_run:cmd("restart server default")
+net = require('net.box')
+test_run = require('test_run').new()
+conn = net.connect(box.cfg.listen)
+conn:call("test", {{address = "Moscow Dolgoprudny"}})
+conn:close()
+box.schema.func.drop('test')
+box.schema.func.exists('test')
+box.schema.user.revoke('guest', 'execute', 'universe')
--
2.21.0
More information about the Tarantool-patches
mailing list