summaryrefslogtreecommitdiffstats
path: root/src/script_lua.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/script_lua.c')
-rw-r--r--src/script_lua.c1741
1 files changed, 1741 insertions, 0 deletions
diff --git a/src/script_lua.c b/src/script_lua.c
new file mode 100644
index 0000000..33ed2aa
--- /dev/null
+++ b/src/script_lua.c
@@ -0,0 +1,1741 @@
+/*
+ * Copyright (c) 2009-2021, Redis Ltd.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of Redis nor the names of its contributors may be used
+ * to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "script_lua.h"
+
+#include "server.h"
+#include "sha1.h"
+#include "rand.h"
+#include "cluster.h"
+#include "monotonic.h"
+#include "resp_parser.h"
+#include "version.h"
+#include <lauxlib.h>
+#include <lualib.h>
+#include <ctype.h>
+#include <math.h>
+
+/* Globals that are added by the Lua libraries */
+static char *libraries_allow_list[] = {
+ "string",
+ "cjson",
+ "bit",
+ "cmsgpack",
+ "math",
+ "table",
+ "struct",
+ NULL,
+};
+
+/* Redis Lua API globals */
+static char *redis_api_allow_list[] = {
+ "redis",
+ "__redis__err__handler", /* error handler for eval, currently located on globals.
+ Should move to registry. */
+ NULL,
+};
+
+/* Lua builtins */
+static char *lua_builtins_allow_list[] = {
+ "xpcall",
+ "tostring",
+ "getfenv",
+ "setmetatable",
+ "next",
+ "assert",
+ "tonumber",
+ "rawequal",
+ "collectgarbage",
+ "getmetatable",
+ "rawset",
+ "pcall",
+ "coroutine",
+ "type",
+ "_G",
+ "select",
+ "unpack",
+ "gcinfo",
+ "pairs",
+ "rawget",
+ "loadstring",
+ "ipairs",
+ "_VERSION",
+ "setfenv",
+ "load",
+ "error",
+ NULL,
+};
+
+/* Lua builtins which are not documented on the Lua documentation */
+static char *lua_builtins_not_documented_allow_list[] = {
+ "newproxy",
+ NULL,
+};
+
+/* Lua builtins which are allowed on initialization but will be removed right after */
+static char *lua_builtins_removed_after_initialization_allow_list[] = {
+ "debug", /* debug will be set to nil after the error handler will be created */
+ NULL,
+};
+
+/* Those allow lists was created from the globals that was
+ * available to the user when the allow lists was first introduce.
+ * Because we do not want to break backward compatibility we keep
+ * all the globals. The allow lists will prevent us from accidentally
+ * creating unwanted globals in the future.
+ *
+ * Also notice that the allow list is only checked on start time,
+ * after that the global table is locked so not need to check anything.*/
+static char **allow_lists[] = {
+ libraries_allow_list,
+ redis_api_allow_list,
+ lua_builtins_allow_list,
+ lua_builtins_not_documented_allow_list,
+ lua_builtins_removed_after_initialization_allow_list,
+ NULL,
+};
+
+/* Deny list contains elements which we know we do not want to add to globals
+ * and there is no need to print a warning message form them. We will print a
+ * log message only if an element was added to the globals and the element is
+ * not on the allow list nor on the back list. */
+static char *deny_list[] = {
+ "dofile",
+ "loadfile",
+ "print",
+ NULL,
+};
+
+static int redis_math_random (lua_State *L);
+static int redis_math_randomseed (lua_State *L);
+static void redisProtocolToLuaType_Int(void *ctx, long long val, const char *proto, size_t proto_len);
+static void redisProtocolToLuaType_BulkString(void *ctx, const char *str, size_t len, const char *proto, size_t proto_len);
+static void redisProtocolToLuaType_NullBulkString(void *ctx, const char *proto, size_t proto_len);
+static void redisProtocolToLuaType_NullArray(void *ctx, const char *proto, size_t proto_len);
+static void redisProtocolToLuaType_Status(void *ctx, const char *str, size_t len, const char *proto, size_t proto_len);
+static void redisProtocolToLuaType_Error(void *ctx, const char *str, size_t len, const char *proto, size_t proto_len);
+static void redisProtocolToLuaType_Array(struct ReplyParser *parser, void *ctx, size_t len, const char *proto);
+static void redisProtocolToLuaType_Map(struct ReplyParser *parser, void *ctx, size_t len, const char *proto);
+static void redisProtocolToLuaType_Set(struct ReplyParser *parser, void *ctx, size_t len, const char *proto);
+static void redisProtocolToLuaType_Null(void *ctx, const char *proto, size_t proto_len);
+static void redisProtocolToLuaType_Bool(void *ctx, int val, const char *proto, size_t proto_len);
+static void redisProtocolToLuaType_Double(void *ctx, double d, const char *proto, size_t proto_len);
+static void redisProtocolToLuaType_BigNumber(void *ctx, const char *str, size_t len, const char *proto, size_t proto_len);
+static void redisProtocolToLuaType_VerbatimString(void *ctx, const char *format, const char *str, size_t len, const char *proto, size_t proto_len);
+static void redisProtocolToLuaType_Attribute(struct ReplyParser *parser, void *ctx, size_t len, const char *proto);
+static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lua);
+
+/*
+ * Save the give pointer on Lua registry, used to save the Lua context and
+ * function context so we can retrieve them from lua_State.
+ */
+void luaSaveOnRegistry(lua_State* lua, const char* name, void* ptr) {
+ lua_pushstring(lua, name);
+ if (ptr) {
+ lua_pushlightuserdata(lua, ptr);
+ } else {
+ lua_pushnil(lua);
+ }
+ lua_settable(lua, LUA_REGISTRYINDEX);
+}
+
+/*
+ * Get a saved pointer from registry
+ */
+void* luaGetFromRegistry(lua_State* lua, const char* name) {
+ lua_pushstring(lua, name);
+ lua_gettable(lua, LUA_REGISTRYINDEX);
+
+ if (lua_isnil(lua, -1)) {
+ return NULL;
+ }
+ /* must be light user data */
+ serverAssert(lua_islightuserdata(lua, -1));
+
+ void* ptr = (void*) lua_topointer(lua, -1);
+ serverAssert(ptr);
+
+ /* pops the value */
+ lua_pop(lua, 1);
+
+ return ptr;
+}
+
+/* ---------------------------------------------------------------------------
+ * Redis reply to Lua type conversion functions.
+ * ------------------------------------------------------------------------- */
+
+/* Take a Redis reply in the Redis protocol format and convert it into a
+ * Lua type. Thanks to this function, and the introduction of not connected
+ * clients, it is trivial to implement the redis() lua function.
+ *
+ * Basically we take the arguments, execute the Redis command in the context
+ * of a non connected client, then take the generated reply and convert it
+ * into a suitable Lua type. With this trick the scripting feature does not
+ * need the introduction of a full Redis internals API. The script
+ * is like a normal client that bypasses all the slow I/O paths.
+ *
+ * Note: in this function we do not do any sanity check as the reply is
+ * generated by Redis directly. This allows us to go faster.
+ *
+ * Errors are returned as a table with a single 'err' field set to the
+ * error string.
+ */
+
+static const ReplyParserCallbacks DefaultLuaTypeParserCallbacks = {
+ .null_array_callback = redisProtocolToLuaType_NullArray,
+ .bulk_string_callback = redisProtocolToLuaType_BulkString,
+ .null_bulk_string_callback = redisProtocolToLuaType_NullBulkString,
+ .error_callback = redisProtocolToLuaType_Error,
+ .simple_str_callback = redisProtocolToLuaType_Status,
+ .long_callback = redisProtocolToLuaType_Int,
+ .array_callback = redisProtocolToLuaType_Array,
+ .set_callback = redisProtocolToLuaType_Set,
+ .map_callback = redisProtocolToLuaType_Map,
+ .bool_callback = redisProtocolToLuaType_Bool,
+ .double_callback = redisProtocolToLuaType_Double,
+ .null_callback = redisProtocolToLuaType_Null,
+ .big_number_callback = redisProtocolToLuaType_BigNumber,
+ .verbatim_string_callback = redisProtocolToLuaType_VerbatimString,
+ .attribute_callback = redisProtocolToLuaType_Attribute,
+ .error = NULL,
+};
+
+static void redisProtocolToLuaType(lua_State *lua, char* reply) {
+ ReplyParser parser = {.curr_location = reply, .callbacks = DefaultLuaTypeParserCallbacks};
+
+ parseReply(&parser, lua);
+}
+
+static void redisProtocolToLuaType_Int(void *ctx, long long val, const char *proto, size_t proto_len) {
+ UNUSED(proto);
+ UNUSED(proto_len);
+ if (!ctx) {
+ return;
+ }
+
+ lua_State *lua = ctx;
+ if (!lua_checkstack(lua, 1)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic. */
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ lua_pushnumber(lua,(lua_Number)val);
+}
+
+static void redisProtocolToLuaType_NullBulkString(void *ctx, const char *proto, size_t proto_len) {
+ UNUSED(proto);
+ UNUSED(proto_len);
+ if (!ctx) {
+ return;
+ }
+
+ lua_State *lua = ctx;
+ if (!lua_checkstack(lua, 1)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic. */
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ lua_pushboolean(lua,0);
+}
+
+static void redisProtocolToLuaType_NullArray(void *ctx, const char *proto, size_t proto_len) {
+ UNUSED(proto);
+ UNUSED(proto_len);
+ if (!ctx) {
+ return;
+ }
+ lua_State *lua = ctx;
+ if (!lua_checkstack(lua, 1)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic. */
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ lua_pushboolean(lua,0);
+}
+
+
+static void redisProtocolToLuaType_BulkString(void *ctx, const char *str, size_t len, const char *proto, size_t proto_len) {
+ UNUSED(proto);
+ UNUSED(proto_len);
+ if (!ctx) {
+ return;
+ }
+
+ lua_State *lua = ctx;
+ if (!lua_checkstack(lua, 1)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic. */
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ lua_pushlstring(lua,str,len);
+}
+
+static void redisProtocolToLuaType_Status(void *ctx, const char *str, size_t len, const char *proto, size_t proto_len) {
+ UNUSED(proto);
+ UNUSED(proto_len);
+ if (!ctx) {
+ return;
+ }
+
+ lua_State *lua = ctx;
+ if (!lua_checkstack(lua, 3)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic. */
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ lua_newtable(lua);
+ lua_pushstring(lua,"ok");
+ lua_pushlstring(lua,str,len);
+ lua_settable(lua,-3);
+}
+
+static void redisProtocolToLuaType_Error(void *ctx, const char *str, size_t len, const char *proto, size_t proto_len) {
+ UNUSED(proto);
+ UNUSED(proto_len);
+ if (!ctx) {
+ return;
+ }
+
+ lua_State *lua = ctx;
+ if (!lua_checkstack(lua, 3)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic. */
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ sds err_msg = sdscatlen(sdsnew("-"), str, len);
+ luaPushErrorBuff(lua,err_msg);
+ /* push a field indicate to ignore updating the stats on this error
+ * because it was already updated when executing the command. */
+ lua_pushstring(lua,"ignore_error_stats_update");
+ lua_pushboolean(lua, 1);
+ lua_settable(lua,-3);
+}
+
+static void redisProtocolToLuaType_Map(struct ReplyParser *parser, void *ctx, size_t len, const char *proto) {
+ UNUSED(proto);
+ lua_State *lua = ctx;
+ if (lua) {
+ if (!lua_checkstack(lua, 3)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic. */
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ lua_newtable(lua);
+ lua_pushstring(lua, "map");
+ lua_newtable(lua);
+ }
+ for (size_t j = 0; j < len; j++) {
+ parseReply(parser,lua);
+ parseReply(parser,lua);
+ if (lua) lua_settable(lua,-3);
+ }
+ if (lua) lua_settable(lua,-3);
+}
+
+static void redisProtocolToLuaType_Set(struct ReplyParser *parser, void *ctx, size_t len, const char *proto) {
+ UNUSED(proto);
+
+ lua_State *lua = ctx;
+ if (lua) {
+ if (!lua_checkstack(lua, 3)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic. */
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ lua_newtable(lua);
+ lua_pushstring(lua, "set");
+ lua_newtable(lua);
+ }
+ for (size_t j = 0; j < len; j++) {
+ parseReply(parser,lua);
+ if (lua) {
+ if (!lua_checkstack(lua, 1)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic.
+ * Notice that here we need to check the stack again because the recursive
+ * call to redisProtocolToLuaType might have use the room allocated in the stack*/
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ lua_pushboolean(lua,1);
+ lua_settable(lua,-3);
+ }
+ }
+ if (lua) lua_settable(lua,-3);
+}
+
+static void redisProtocolToLuaType_Array(struct ReplyParser *parser, void *ctx, size_t len, const char *proto) {
+ UNUSED(proto);
+
+ lua_State *lua = ctx;
+ if (lua){
+ if (!lua_checkstack(lua, 2)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic. */
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ lua_newtable(lua);
+ }
+ for (size_t j = 0; j < len; j++) {
+ if (lua) lua_pushnumber(lua,j+1);
+ parseReply(parser,lua);
+ if (lua) lua_settable(lua,-3);
+ }
+}
+
+static void redisProtocolToLuaType_Attribute(struct ReplyParser *parser, void *ctx, size_t len, const char *proto) {
+ UNUSED(proto);
+
+ /* Parse the attribute reply.
+ * Currently, we do not expose the attribute to the Lua script so
+ * we just need to continue parsing and ignore it (the NULL ensures that the
+ * reply will be ignored). */
+ for (size_t j = 0; j < len; j++) {
+ parseReply(parser,NULL);
+ parseReply(parser,NULL);
+ }
+
+ /* Parse the reply itself. */
+ parseReply(parser,ctx);
+}
+
+static void redisProtocolToLuaType_VerbatimString(void *ctx, const char *format, const char *str, size_t len, const char *proto, size_t proto_len) {
+ UNUSED(proto);
+ UNUSED(proto_len);
+ if (!ctx) {
+ return;
+ }
+
+ lua_State *lua = ctx;
+ if (!lua_checkstack(lua, 5)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic. */
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ lua_newtable(lua);
+ lua_pushstring(lua,"verbatim_string");
+ lua_newtable(lua);
+ lua_pushstring(lua,"string");
+ lua_pushlstring(lua,str,len);
+ lua_settable(lua,-3);
+ lua_pushstring(lua,"format");
+ lua_pushlstring(lua,format,3);
+ lua_settable(lua,-3);
+ lua_settable(lua,-3);
+}
+
+static void redisProtocolToLuaType_BigNumber(void *ctx, const char *str, size_t len, const char *proto, size_t proto_len) {
+ UNUSED(proto);
+ UNUSED(proto_len);
+ if (!ctx) {
+ return;
+ }
+
+ lua_State *lua = ctx;
+ if (!lua_checkstack(lua, 3)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic. */
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ lua_newtable(lua);
+ lua_pushstring(lua,"big_number");
+ lua_pushlstring(lua,str,len);
+ lua_settable(lua,-3);
+}
+
+static void redisProtocolToLuaType_Null(void *ctx, const char *proto, size_t proto_len) {
+ UNUSED(proto);
+ UNUSED(proto_len);
+ if (!ctx) {
+ return;
+ }
+
+ lua_State *lua = ctx;
+ if (!lua_checkstack(lua, 1)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic. */
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ lua_pushnil(lua);
+}
+
+static void redisProtocolToLuaType_Bool(void *ctx, int val, const char *proto, size_t proto_len) {
+ UNUSED(proto);
+ UNUSED(proto_len);
+ if (!ctx) {
+ return;
+ }
+
+ lua_State *lua = ctx;
+ if (!lua_checkstack(lua, 1)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic. */
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ lua_pushboolean(lua,val);
+}
+
+static void redisProtocolToLuaType_Double(void *ctx, double d, const char *proto, size_t proto_len) {
+ UNUSED(proto);
+ UNUSED(proto_len);
+ if (!ctx) {
+ return;
+ }
+
+ lua_State *lua = ctx;
+ if (!lua_checkstack(lua, 3)) {
+ /* Increase the Lua stack if needed, to make sure there is enough room
+ * to push elements to the stack. On failure, exit with panic. */
+ serverPanic("lua stack limit reach when parsing redis.call reply");
+ }
+ lua_newtable(lua);
+ lua_pushstring(lua,"double");
+ lua_pushnumber(lua,d);
+ lua_settable(lua,-3);
+}
+
+/* This function is used in order to push an error on the Lua stack in the
+ * format used by redis.pcall to return errors, which is a lua table
+ * with an "err" field set to the error string including the error code.
+ * Note that this table is never a valid reply by proper commands,
+ * since the returned tables are otherwise always indexed by integers, never by strings.
+ *
+ * The function takes ownership on the given err_buffer. */
+void luaPushErrorBuff(lua_State *lua, sds err_buffer) {
+ sds msg;
+ sds error_code;
+
+ /* If debugging is active and in step mode, log errors resulting from
+ * Redis commands. */
+ if (ldbIsEnabled()) {
+ ldbLog(sdscatprintf(sdsempty(),"<error> %s",err_buffer));
+ }
+
+ /* There are two possible formats for the received `error` string:
+ * 1) "-CODE msg": in this case we remove the leading '-' since we don't store it as part of the lua error format.
+ * 2) "msg": in this case we prepend a generic 'ERR' code since all error statuses need some error code.
+ * We support format (1) so this function can reuse the error messages used in other places in redis.
+ * We support format (2) so it'll be easy to pass descriptive errors to this function without worrying about format.
+ */
+ if (err_buffer[0] == '-') {
+ /* derive error code from the message */
+ char *err_msg = strstr(err_buffer, " ");
+ if (!err_msg) {
+ msg = sdsnew(err_buffer+1);
+ error_code = sdsnew("ERR");
+ } else {
+ *err_msg = '\0';
+ msg = sdsnew(err_msg+1);
+ error_code = sdsnew(err_buffer + 1);
+ }
+ sdsfree(err_buffer);
+ } else {
+ msg = err_buffer;
+ error_code = sdsnew("ERR");
+ }
+ /* Trim newline at end of string. If we reuse the ready-made Redis error objects (case 1 above) then we might
+ * have a newline that needs to be trimmed. In any case the lua Redis error table shouldn't end with a newline. */
+ msg = sdstrim(msg, "\r\n");
+ sds final_msg = sdscatfmt(error_code, " %s", msg);
+
+ lua_newtable(lua);
+ lua_pushstring(lua,"err");
+ lua_pushstring(lua, final_msg);
+ lua_settable(lua,-3);
+
+ sdsfree(msg);
+ sdsfree(final_msg);
+}
+
+void luaPushError(lua_State *lua, const char *error) {
+ luaPushErrorBuff(lua, sdsnew(error));
+}
+
+/* In case the error set into the Lua stack by luaPushError() was generated
+ * by the non-error-trapping version of redis.pcall(), which is redis.call(),
+ * this function will raise the Lua error so that the execution of the
+ * script will be halted. */
+int luaError(lua_State *lua) {
+ return lua_error(lua);
+}
+
+
+/* ---------------------------------------------------------------------------
+ * Lua reply to Redis reply conversion functions.
+ * ------------------------------------------------------------------------- */
+
+/* Reply to client 'c' converting the top element in the Lua stack to a
+ * Redis reply. As a side effect the element is consumed from the stack. */
+static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lua) {
+ int t = lua_type(lua,-1);
+
+ if (!lua_checkstack(lua, 4)) {
+ /* Increase the Lua stack if needed to make sure there is enough room
+ * to push 4 elements to the stack. On failure, return error.
+ * Notice that we need, in the worst case, 4 elements because returning a map might
+ * require push 4 elements to the Lua stack.*/
+ addReplyErrorFormat(c, "reached lua stack limit");
+ lua_pop(lua,1); /* pop the element from the stack */
+ return;
+ }
+
+ switch(t) {
+ case LUA_TSTRING:
+ addReplyBulkCBuffer(c,(char*)lua_tostring(lua,-1),lua_strlen(lua,-1));
+ break;
+ case LUA_TBOOLEAN:
+ if (script_client->resp == 2)
+ addReply(c,lua_toboolean(lua,-1) ? shared.cone :
+ shared.null[c->resp]);
+ else
+ addReplyBool(c,lua_toboolean(lua,-1));
+ break;
+ case LUA_TNUMBER:
+ addReplyLongLong(c,(long long)lua_tonumber(lua,-1));
+ break;
+ case LUA_TTABLE:
+ /* We need to check if it is an array, an error, or a status reply.
+ * Error are returned as a single element table with 'err' field.
+ * Status replies are returned as single element table with 'ok'
+ * field. */
+
+ /* Handle error reply. */
+ /* we took care of the stack size on function start */
+ lua_pushstring(lua,"err");
+ lua_rawget(lua,-2);
+ t = lua_type(lua,-1);
+ if (t == LUA_TSTRING) {
+ lua_pop(lua, 1); /* pop the error message, we will use luaExtractErrorInformation to get error information */
+ errorInfo err_info = {0};
+ luaExtractErrorInformation(lua, &err_info);
+ addReplyErrorFormatEx(c,
+ err_info.ignore_err_stats_update? ERR_REPLY_FLAG_NO_STATS_UPDATE: 0,
+ "-%s",
+ err_info.msg);
+ luaErrorInformationDiscard(&err_info);
+ lua_pop(lua,1); /* pop the result table */
+ return;
+ }
+ lua_pop(lua,1); /* Discard field name pushed before. */
+
+ /* Handle status reply. */
+ lua_pushstring(lua,"ok");
+ lua_rawget(lua,-2);
+ t = lua_type(lua,-1);
+ if (t == LUA_TSTRING) {
+ sds ok = sdsnew(lua_tostring(lua,-1));
+ sdsmapchars(ok,"\r\n"," ",2);
+ addReplyStatusLength(c, ok, sdslen(ok));
+ sdsfree(ok);
+ lua_pop(lua,2);
+ return;
+ }
+ lua_pop(lua,1); /* Discard field name pushed before. */
+
+ /* Handle double reply. */
+ lua_pushstring(lua,"double");
+ lua_rawget(lua,-2);
+ t = lua_type(lua,-1);
+ if (t == LUA_TNUMBER) {
+ addReplyDouble(c,lua_tonumber(lua,-1));
+ lua_pop(lua,2);
+ return;
+ }
+ lua_pop(lua,1); /* Discard field name pushed before. */
+
+ /* Handle big number reply. */
+ lua_pushstring(lua,"big_number");
+ lua_rawget(lua,-2);
+ t = lua_type(lua,-1);
+ if (t == LUA_TSTRING) {
+ sds big_num = sdsnewlen(lua_tostring(lua,-1), lua_strlen(lua,-1));
+ sdsmapchars(big_num,"\r\n"," ",2);
+ addReplyBigNum(c,big_num,sdslen(big_num));
+ sdsfree(big_num);
+ lua_pop(lua,2);
+ return;
+ }
+ lua_pop(lua,1); /* Discard field name pushed before. */
+
+ /* Handle verbatim reply. */
+ lua_pushstring(lua,"verbatim_string");
+ lua_rawget(lua,-2);
+ t = lua_type(lua,-1);
+ if (t == LUA_TTABLE) {
+ lua_pushstring(lua,"format");
+ lua_rawget(lua,-2);
+ t = lua_type(lua,-1);
+ if (t == LUA_TSTRING){
+ char* format = (char*)lua_tostring(lua,-1);
+ lua_pushstring(lua,"string");
+ lua_rawget(lua,-3);
+ t = lua_type(lua,-1);
+ if (t == LUA_TSTRING){
+ size_t len;
+ char* str = (char*)lua_tolstring(lua,-1,&len);
+ addReplyVerbatim(c, str, len, format);
+ lua_pop(lua,4);
+ return;
+ }
+ lua_pop(lua,1);
+ }
+ lua_pop(lua,1);
+ }
+ lua_pop(lua,1); /* Discard field name pushed before. */
+
+ /* Handle map reply. */
+ lua_pushstring(lua,"map");
+ lua_rawget(lua,-2);
+ t = lua_type(lua,-1);
+ if (t == LUA_TTABLE) {
+ int maplen = 0;
+ void *replylen = addReplyDeferredLen(c);
+ /* we took care of the stack size on function start */
+ lua_pushnil(lua); /* Use nil to start iteration. */
+ while (lua_next(lua,-2)) {
+ /* Stack now: table, key, value */
+ lua_pushvalue(lua,-2); /* Dup key before consuming. */
+ luaReplyToRedisReply(c, script_client, lua); /* Return key. */
+ luaReplyToRedisReply(c, script_client, lua); /* Return value. */
+ /* Stack now: table, key. */
+ maplen++;
+ }
+ setDeferredMapLen(c,replylen,maplen);
+ lua_pop(lua,2);
+ return;
+ }
+ lua_pop(lua,1); /* Discard field name pushed before. */
+
+ /* Handle set reply. */
+ lua_pushstring(lua,"set");
+ lua_rawget(lua,-2);
+ t = lua_type(lua,-1);
+ if (t == LUA_TTABLE) {
+ int setlen = 0;
+ void *replylen = addReplyDeferredLen(c);
+ /* we took care of the stack size on function start */
+ lua_pushnil(lua); /* Use nil to start iteration. */
+ while (lua_next(lua,-2)) {
+ /* Stack now: table, key, true */
+ lua_pop(lua,1); /* Discard the boolean value. */
+ lua_pushvalue(lua,-1); /* Dup key before consuming. */
+ luaReplyToRedisReply(c, script_client, lua); /* Return key. */
+ /* Stack now: table, key. */
+ setlen++;
+ }
+ setDeferredSetLen(c,replylen,setlen);
+ lua_pop(lua,2);
+ return;
+ }
+ lua_pop(lua,1); /* Discard field name pushed before. */
+
+ /* Handle the array reply. */
+ void *replylen = addReplyDeferredLen(c);
+ int j = 1, mbulklen = 0;
+ while(1) {
+ /* we took care of the stack size on function start */
+ lua_pushnumber(lua,j++);
+ lua_rawget(lua,-2);
+ t = lua_type(lua,-1);
+ if (t == LUA_TNIL) {
+ lua_pop(lua,1);
+ break;
+ }
+ luaReplyToRedisReply(c, script_client, lua);
+ mbulklen++;
+ }
+ setDeferredArrayLen(c,replylen,mbulklen);
+ break;
+ default:
+ addReplyNull(c);
+ }
+ lua_pop(lua,1);
+}
+
+/* ---------------------------------------------------------------------------
+ * Lua redis.* functions implementations.
+ * ------------------------------------------------------------------------- */
+void freeLuaRedisArgv(robj **argv, int argc, int argv_len);
+
+/* Cached argv array across calls. */
+static robj **lua_argv = NULL;
+static int lua_argv_size = 0;
+
+/* Cache of recently used small arguments to avoid malloc calls. */
+#define LUA_CMD_OBJCACHE_SIZE 32
+#define LUA_CMD_OBJCACHE_MAX_LEN 64
+static robj *lua_args_cached_objects[LUA_CMD_OBJCACHE_SIZE];
+static size_t lua_args_cached_objects_len[LUA_CMD_OBJCACHE_SIZE];
+
+static robj **luaArgsToRedisArgv(lua_State *lua, int *argc, int *argv_len) {
+ int j;
+ /* Require at least one argument */
+ *argc = lua_gettop(lua);
+ if (*argc == 0) {
+ luaPushError(lua, "Please specify at least one argument for this redis lib call");
+ return NULL;
+ }
+
+ /* Build the arguments vector (reuse a cached argv from last call) */
+ if (lua_argv_size < *argc) {
+ lua_argv = zrealloc(lua_argv,sizeof(robj*)* *argc);
+ lua_argv_size = *argc;
+ }
+ *argv_len = lua_argv_size;
+
+ for (j = 0; j < *argc; j++) {
+ char *obj_s;
+ size_t obj_len;
+ char dbuf[64];
+
+ if (lua_type(lua,j+1) == LUA_TNUMBER) {
+ /* We can't use lua_tolstring() for number -> string conversion
+ * since Lua uses a format specifier that loses precision. */
+ lua_Number num = lua_tonumber(lua,j+1);
+
+ obj_len = snprintf(dbuf,sizeof(dbuf),"%.17g",(double)num);
+ obj_s = dbuf;
+ } else {
+ obj_s = (char*)lua_tolstring(lua,j+1,&obj_len);
+ if (obj_s == NULL) break; /* Not a string. */
+ }
+ /* Try to use a cached object. */
+ if (j < LUA_CMD_OBJCACHE_SIZE && lua_args_cached_objects[j] &&
+ lua_args_cached_objects_len[j] >= obj_len)
+ {
+ sds s = lua_args_cached_objects[j]->ptr;
+ lua_argv[j] = lua_args_cached_objects[j];
+ lua_args_cached_objects[j] = NULL;
+ memcpy(s,obj_s,obj_len+1);
+ sdssetlen(s, obj_len);
+ } else {
+ lua_argv[j] = createStringObject(obj_s, obj_len);
+ }
+ }
+
+ /* Pop all arguments from the stack, we do not need them anymore
+ * and this way we guaranty we will have room on the stack for the result. */
+ lua_pop(lua, *argc);
+
+ /* Check if one of the arguments passed by the Lua script
+ * is not a string or an integer (lua_isstring() return true for
+ * integers as well). */
+ if (j != *argc) {
+ freeLuaRedisArgv(lua_argv, j, lua_argv_size);
+ luaPushError(lua, "Lua redis lib command arguments must be strings or integers");
+ return NULL;
+ }
+
+ return lua_argv;
+}
+
+void freeLuaRedisArgv(robj **argv, int argc, int argv_len) {
+ int j;
+ for (j = 0; j < argc; j++) {
+ robj *o = argv[j];
+
+ /* Try to cache the object in the lua_args_cached_objects array.
+ * The object must be small, SDS-encoded, and with refcount = 1
+ * (we must be the only owner) for us to cache it. */
+ if (j < LUA_CMD_OBJCACHE_SIZE &&
+ o->refcount == 1 &&
+ (o->encoding == OBJ_ENCODING_RAW ||
+ o->encoding == OBJ_ENCODING_EMBSTR) &&
+ sdslen(o->ptr) <= LUA_CMD_OBJCACHE_MAX_LEN)
+ {
+ sds s = o->ptr;
+ if (lua_args_cached_objects[j]) decrRefCount(lua_args_cached_objects[j]);
+ lua_args_cached_objects[j] = o;
+ lua_args_cached_objects_len[j] = sdsalloc(s);
+ } else {
+ decrRefCount(o);
+ }
+ }
+ if (argv != lua_argv || argv_len != lua_argv_size) {
+ /* The command changed argv, scrap the cache and start over. */
+ zfree(argv);
+ lua_argv = NULL;
+ lua_argv_size = 0;
+ }
+}
+
+static int luaRedisGenericCommand(lua_State *lua, int raise_error) {
+ int j;
+ scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME);
+ if (!rctx) {
+ luaPushError(lua, "redis.call/pcall can only be called inside a script invocation");
+ return luaError(lua);
+ }
+ sds err = NULL;
+ client* c = rctx->c;
+ sds reply;
+
+ c->argv = luaArgsToRedisArgv(lua, &c->argc, &c->argv_len);
+ if (c->argv == NULL) {
+ return raise_error ? luaError(lua) : 1;
+ }
+
+ static int inuse = 0; /* Recursive calls detection. */
+
+ /* By using Lua debug hooks it is possible to trigger a recursive call
+ * to luaRedisGenericCommand(), which normally should never happen.
+ * To make this function reentrant is futile and makes it slower, but
+ * we should at least detect such a misuse, and abort. */
+ if (inuse) {
+ char *recursion_warning =
+ "luaRedisGenericCommand() recursive call detected. "
+ "Are you doing funny stuff with Lua debug hooks?";
+ serverLog(LL_WARNING,"%s",recursion_warning);
+ luaPushError(lua,recursion_warning);
+ return 1;
+ }
+ inuse++;
+
+ /* Log the command if debugging is active. */
+ if (ldbIsEnabled()) {
+ sds cmdlog = sdsnew("<redis>");
+ for (j = 0; j < c->argc; j++) {
+ if (j == 10) {
+ cmdlog = sdscatprintf(cmdlog," ... (%d more)",
+ c->argc-j-1);
+ break;
+ } else {
+ cmdlog = sdscatlen(cmdlog," ",1);
+ cmdlog = sdscatsds(cmdlog,c->argv[j]->ptr);
+ }
+ }
+ ldbLog(cmdlog);
+ }
+
+ scriptCall(rctx, &err);
+ if (err) {
+ luaPushError(lua, err);
+ sdsfree(err);
+ /* push a field indicate to ignore updating the stats on this error
+ * because it was already updated when executing the command. */
+ lua_pushstring(lua,"ignore_error_stats_update");
+ lua_pushboolean(lua, 1);
+ lua_settable(lua,-3);
+ goto cleanup;
+ }
+
+ /* Convert the result of the Redis command into a suitable Lua type.
+ * The first thing we need is to create a single string from the client
+ * output buffers. */
+ if (listLength(c->reply) == 0 && (size_t)c->bufpos < c->buf_usable_size) {
+ /* This is a fast path for the common case of a reply inside the
+ * client static buffer. Don't create an SDS string but just use
+ * the client buffer directly. */
+ c->buf[c->bufpos] = '\0';
+ reply = c->buf;
+ c->bufpos = 0;
+ } else {
+ reply = sdsnewlen(c->buf,c->bufpos);
+ c->bufpos = 0;
+ while(listLength(c->reply)) {
+ clientReplyBlock *o = listNodeValue(listFirst(c->reply));
+
+ reply = sdscatlen(reply,o->buf,o->used);
+ listDelNode(c->reply,listFirst(c->reply));
+ }
+ }
+ if (raise_error && reply[0] != '-') raise_error = 0;
+ redisProtocolToLuaType(lua,reply);
+
+ /* If the debugger is active, log the reply from Redis. */
+ if (ldbIsEnabled())
+ ldbLogRedisReply(reply);
+
+ if (reply != c->buf) sdsfree(reply);
+ c->reply_bytes = 0;
+
+cleanup:
+ /* Clean up. Command code may have changed argv/argc so we use the
+ * argv/argc of the client instead of the local variables. */
+ freeLuaRedisArgv(c->argv, c->argc, c->argv_len);
+ c->argc = c->argv_len = 0;
+ c->user = NULL;
+ c->argv = NULL;
+ freeClientArgv(c);
+ inuse--;
+
+ if (raise_error) {
+ /* If we are here we should have an error in the stack, in the
+ * form of a table with an "err" field. Extract the string to
+ * return the plain error. */
+ return luaError(lua);
+ }
+ return 1;
+}
+
+/* Our implementation to lua pcall.
+ * We need this implementation for backward
+ * comparability with older Redis versions.
+ *
+ * On Redis 7, the error object is a table,
+ * compare to older version where the error
+ * object is a string. To keep backward
+ * comparability we catch the table object
+ * and just return the error message. */
+static int luaRedisPcall(lua_State *lua) {
+ int argc = lua_gettop(lua);
+ lua_pushboolean(lua, 1); /* result place holder */
+ lua_insert(lua, 1);
+ if (lua_pcall(lua, argc - 1, LUA_MULTRET, 0)) {
+ /* Error */
+ lua_remove(lua, 1); /* remove the result place holder, now we have room for at least one element */
+ if (lua_istable(lua, -1)) {
+ lua_getfield(lua, -1, "err");
+ if (lua_isstring(lua, -1)) {
+ lua_replace(lua, -2); /* replace the error message with the table */
+ }
+ }
+ lua_pushboolean(lua, 0); /* push result */
+ lua_insert(lua, 1);
+ }
+ return lua_gettop(lua);
+
+}
+
+/* redis.call() */
+static int luaRedisCallCommand(lua_State *lua) {
+ return luaRedisGenericCommand(lua,1);
+}
+
+/* redis.pcall() */
+static int luaRedisPCallCommand(lua_State *lua) {
+ return luaRedisGenericCommand(lua,0);
+}
+
+/* This adds redis.sha1hex(string) to Lua scripts using the same hashing
+ * function used for sha1ing lua scripts. */
+static int luaRedisSha1hexCommand(lua_State *lua) {
+ int argc = lua_gettop(lua);
+ char digest[41];
+ size_t len;
+ char *s;
+
+ if (argc != 1) {
+ luaPushError(lua, "wrong number of arguments");
+ return luaError(lua);
+ }
+
+ s = (char*)lua_tolstring(lua,1,&len);
+ sha1hex(digest,s,len);
+ lua_pushstring(lua,digest);
+ return 1;
+}
+
+/* Returns a table with a single field 'field' set to the string value
+ * passed as argument. This helper function is handy when returning
+ * a Redis Protocol error or status reply from Lua:
+ *
+ * return redis.error_reply("ERR Some Error")
+ * return redis.status_reply("ERR Some Error")
+ */
+static int luaRedisReturnSingleFieldTable(lua_State *lua, char *field) {
+ if (lua_gettop(lua) != 1 || lua_type(lua,-1) != LUA_TSTRING) {
+ luaPushError(lua, "wrong number or type of arguments");
+ return 1;
+ }
+
+ lua_newtable(lua);
+ lua_pushstring(lua, field);
+ lua_pushvalue(lua, -3);
+ lua_settable(lua, -3);
+ return 1;
+}
+
+/* redis.error_reply() */
+static int luaRedisErrorReplyCommand(lua_State *lua) {
+ if (lua_gettop(lua) != 1 || lua_type(lua,-1) != LUA_TSTRING) {
+ luaPushError(lua, "wrong number or type of arguments");
+ return 1;
+ }
+
+ /* add '-' if not exists */
+ const char *err = lua_tostring(lua, -1);
+ sds err_buff = NULL;
+ if (err[0] != '-') {
+ err_buff = sdscatfmt(sdsempty(), "-%s", err);
+ } else {
+ err_buff = sdsnew(err);
+ }
+ luaPushErrorBuff(lua, err_buff);
+ return 1;
+}
+
+/* redis.status_reply() */
+static int luaRedisStatusReplyCommand(lua_State *lua) {
+ return luaRedisReturnSingleFieldTable(lua,"ok");
+}
+
+/* redis.set_repl()
+ *
+ * Set the propagation of write commands executed in the context of the
+ * script to on/off for AOF and slaves. */
+static int luaRedisSetReplCommand(lua_State *lua) {
+ int flags, argc = lua_gettop(lua);
+
+ scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME);
+ if (!rctx) {
+ luaPushError(lua, "redis.set_repl can only be called inside a script invocation");
+ return luaError(lua);
+ }
+
+ if (argc != 1) {
+ luaPushError(lua, "redis.set_repl() requires two arguments.");
+ return luaError(lua);
+ }
+
+ flags = lua_tonumber(lua,-1);
+ if ((flags & ~(PROPAGATE_AOF|PROPAGATE_REPL)) != 0) {
+ luaPushError(lua, "Invalid replication flags. Use REPL_AOF, REPL_REPLICA, REPL_ALL or REPL_NONE.");
+ return luaError(lua);
+ }
+
+ scriptSetRepl(rctx, flags);
+ return 0;
+}
+
+/* redis.acl_check_cmd()
+ *
+ * Checks ACL permissions for given command for the current user. */
+static int luaRedisAclCheckCmdPermissionsCommand(lua_State *lua) {
+ scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME);
+ if (!rctx) {
+ luaPushError(lua, "redis.acl_check_cmd can only be called inside a script invocation");
+ return luaError(lua);
+ }
+ int raise_error = 0;
+
+ int argc, argv_len;
+ robj **argv = luaArgsToRedisArgv(lua, &argc, &argv_len);
+
+ /* Require at least one argument */
+ if (argv == NULL) return luaError(lua);
+
+ /* Find command */
+ struct redisCommand *cmd;
+ if ((cmd = lookupCommand(argv, argc)) == NULL) {
+ luaPushError(lua, "Invalid command passed to redis.acl_check_cmd()");
+ raise_error = 1;
+ } else {
+ int keyidxptr;
+ if (ACLCheckAllUserCommandPerm(rctx->original_client->user, cmd, argv, argc, &keyidxptr) != ACL_OK) {
+ lua_pushboolean(lua, 0);
+ } else {
+ lua_pushboolean(lua, 1);
+ }
+ }
+
+ freeLuaRedisArgv(argv, argc, argv_len);
+ if (raise_error)
+ return luaError(lua);
+ else
+ return 1;
+}
+
+
+/* redis.log() */
+static int luaLogCommand(lua_State *lua) {
+ int j, argc = lua_gettop(lua);
+ int level;
+ sds log;
+
+ if (argc < 2) {
+ luaPushError(lua, "redis.log() requires two arguments or more.");
+ return luaError(lua);
+ } else if (!lua_isnumber(lua,-argc)) {
+ luaPushError(lua, "First argument must be a number (log level).");
+ return luaError(lua);
+ }
+ level = lua_tonumber(lua,-argc);
+ if (level < LL_DEBUG || level > LL_WARNING) {
+ luaPushError(lua, "Invalid debug level.");
+ return luaError(lua);
+ }
+ if (level < server.verbosity) return 0;
+
+ /* Glue together all the arguments */
+ log = sdsempty();
+ for (j = 1; j < argc; j++) {
+ size_t len;
+ char *s;
+
+ s = (char*)lua_tolstring(lua,(-argc)+j,&len);
+ if (s) {
+ if (j != 1) log = sdscatlen(log," ",1);
+ log = sdscatlen(log,s,len);
+ }
+ }
+ serverLogRaw(level,log);
+ sdsfree(log);
+ return 0;
+}
+
+/* redis.setresp() */
+static int luaSetResp(lua_State *lua) {
+ scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME);
+ if (!rctx) {
+ luaPushError(lua, "redis.setresp can only be called inside a script invocation");
+ return luaError(lua);
+ }
+ int argc = lua_gettop(lua);
+
+ if (argc != 1) {
+ luaPushError(lua, "redis.setresp() requires one argument.");
+ return luaError(lua);
+ }
+
+ int resp = lua_tonumber(lua,-argc);
+ if (resp != 2 && resp != 3) {
+ luaPushError(lua, "RESP version must be 2 or 3.");
+ return luaError(lua);
+ }
+ scriptSetResp(rctx, resp);
+ return 0;
+}
+
+/* ---------------------------------------------------------------------------
+ * Lua engine initialization and reset.
+ * ------------------------------------------------------------------------- */
+
+static void luaLoadLib(lua_State *lua, const char *libname, lua_CFunction luafunc) {
+ lua_pushcfunction(lua, luafunc);
+ lua_pushstring(lua, libname);
+ lua_call(lua, 1, 0);
+}
+
+LUALIB_API int (luaopen_cjson) (lua_State *L);
+LUALIB_API int (luaopen_struct) (lua_State *L);
+LUALIB_API int (luaopen_cmsgpack) (lua_State *L);
+LUALIB_API int (luaopen_bit) (lua_State *L);
+
+static void luaLoadLibraries(lua_State *lua) {
+ luaLoadLib(lua, "", luaopen_base);
+ luaLoadLib(lua, LUA_TABLIBNAME, luaopen_table);
+ luaLoadLib(lua, LUA_STRLIBNAME, luaopen_string);
+ luaLoadLib(lua, LUA_MATHLIBNAME, luaopen_math);
+ luaLoadLib(lua, LUA_DBLIBNAME, luaopen_debug);
+ luaLoadLib(lua, "cjson", luaopen_cjson);
+ luaLoadLib(lua, "struct", luaopen_struct);
+ luaLoadLib(lua, "cmsgpack", luaopen_cmsgpack);
+ luaLoadLib(lua, "bit", luaopen_bit);
+
+#if 0 /* Stuff that we don't load currently, for sandboxing concerns. */
+ luaLoadLib(lua, LUA_LOADLIBNAME, luaopen_package);
+ luaLoadLib(lua, LUA_OSLIBNAME, luaopen_os);
+#endif
+}
+
+/* Return sds of the string value located on stack at the given index.
+ * Return NULL if the value is not a string. */
+sds luaGetStringSds(lua_State *lua, int index) {
+ if (!lua_isstring(lua, index)) {
+ return NULL;
+ }
+
+ size_t len;
+ const char *str = lua_tolstring(lua, index, &len);
+ sds str_sds = sdsnewlen(str, len);
+ return str_sds;
+}
+
+static int luaProtectedTableError(lua_State *lua) {
+ int argc = lua_gettop(lua);
+ if (argc != 2) {
+ serverLog(LL_WARNING, "malicious code trying to call luaProtectedTableError with wrong arguments");
+ luaL_error(lua, "Wrong number of arguments to luaProtectedTableError");
+ }
+ if (!lua_isstring(lua, -1) && !lua_isnumber(lua, -1)) {
+ luaL_error(lua, "Second argument to luaProtectedTableError must be a string or number");
+ }
+ const char *variable_name = lua_tostring(lua, -1);
+ luaL_error(lua, "Script attempted to access nonexistent global variable '%s'", variable_name);
+ return 0;
+}
+
+/* Set a special metatable on the table on the top of the stack.
+ * The metatable will raise an error if the user tries to fetch
+ * an un-existing value.
+ *
+ * The function assumes the Lua stack have a least enough
+ * space to push 2 element, its up to the caller to verify
+ * this before calling this function. */
+void luaSetErrorMetatable(lua_State *lua) {
+ lua_newtable(lua); /* push metatable */
+ lua_pushcfunction(lua, luaProtectedTableError); /* push get error handler */
+ lua_setfield(lua, -2, "__index");
+ lua_setmetatable(lua, -2);
+}
+
+static int luaNewIndexAllowList(lua_State *lua) {
+ int argc = lua_gettop(lua);
+ if (argc != 3) {
+ serverLog(LL_WARNING, "malicious code trying to call luaProtectedTableError with wrong arguments");
+ luaL_error(lua, "Wrong number of arguments to luaNewIndexAllowList");
+ }
+ if (!lua_istable(lua, -3)) {
+ luaL_error(lua, "first argument to luaNewIndexAllowList must be a table");
+ }
+ if (!lua_isstring(lua, -2) && !lua_isnumber(lua, -2)) {
+ luaL_error(lua, "Second argument to luaNewIndexAllowList must be a string or number");
+ }
+ const char *variable_name = lua_tostring(lua, -2);
+ /* check if the key is in our allow list */
+
+ char ***allow_l = allow_lists;
+ for (; *allow_l ; ++allow_l){
+ char **c = *allow_l;
+ for (; *c ; ++c) {
+ if (strcmp(*c, variable_name) == 0) {
+ break;
+ }
+ }
+ if (*c) {
+ break;
+ }
+ }
+ if (!*allow_l) {
+ /* Search the value on the back list, if its there we know that it was removed
+ * on purpose and there is no need to print a warning. */
+ char **c = deny_list;
+ for ( ; *c ; ++c) {
+ if (strcmp(*c, variable_name) == 0) {
+ break;
+ }
+ }
+ if (!*c) {
+ serverLog(LL_WARNING, "A key '%s' was added to Lua globals which is not on the globals allow list nor listed on the deny list.", variable_name);
+ }
+ } else {
+ lua_rawset(lua, -3);
+ }
+ return 0;
+}
+
+/* Set a metatable with '__newindex' function that verify that
+ * the new index appears on our globals while list.
+ *
+ * The metatable is set on the table which located on the top
+ * of the stack.
+ */
+void luaSetAllowListProtection(lua_State *lua) {
+ lua_newtable(lua); /* push metatable */
+ lua_pushcfunction(lua, luaNewIndexAllowList); /* push get error handler */
+ lua_setfield(lua, -2, "__newindex");
+ lua_setmetatable(lua, -2);
+}
+
+/* Set the readonly flag on the table located on the top of the stack
+ * and recursively call this function on each table located on the original
+ * table. Also, recursively call this function on the metatables.*/
+void luaSetTableProtectionRecursively(lua_State *lua) {
+ /* This protect us from a loop in case we already visited the table
+ * For example, globals has '_G' key which is pointing back to globals. */
+ if (lua_isreadonlytable(lua, -1)) {
+ return;
+ }
+
+ /* protect the current table */
+ lua_enablereadonlytable(lua, -1, 1);
+
+ lua_checkstack(lua, 2);
+ lua_pushnil(lua); /* Use nil to start iteration. */
+ while (lua_next(lua,-2)) {
+ /* Stack now: table, key, value */
+ if (lua_istable(lua, -1)) {
+ luaSetTableProtectionRecursively(lua);
+ }
+ lua_pop(lua, 1);
+ }
+
+ /* protect the metatable if exists */
+ if (lua_getmetatable(lua, -1)) {
+ luaSetTableProtectionRecursively(lua);
+ lua_pop(lua, 1); /* pop the metatable */
+ }
+}
+
+void luaRegisterVersion(lua_State* lua) {
+ lua_pushstring(lua,"REDIS_VERSION_NUM");
+ lua_pushnumber(lua,REDIS_VERSION_NUM);
+ lua_settable(lua,-3);
+
+ lua_pushstring(lua,"REDIS_VERSION");
+ lua_pushstring(lua,REDIS_VERSION);
+ lua_settable(lua,-3);
+}
+
+void luaRegisterLogFunction(lua_State* lua) {
+ /* redis.log and log levels. */
+ lua_pushstring(lua,"log");
+ lua_pushcfunction(lua,luaLogCommand);
+ lua_settable(lua,-3);
+
+ lua_pushstring(lua,"LOG_DEBUG");
+ lua_pushnumber(lua,LL_DEBUG);
+ lua_settable(lua,-3);
+
+ lua_pushstring(lua,"LOG_VERBOSE");
+ lua_pushnumber(lua,LL_VERBOSE);
+ lua_settable(lua,-3);
+
+ lua_pushstring(lua,"LOG_NOTICE");
+ lua_pushnumber(lua,LL_NOTICE);
+ lua_settable(lua,-3);
+
+ lua_pushstring(lua,"LOG_WARNING");
+ lua_pushnumber(lua,LL_WARNING);
+ lua_settable(lua,-3);
+}
+
+void luaRegisterRedisAPI(lua_State* lua) {
+ lua_pushvalue(lua, LUA_GLOBALSINDEX);
+ luaSetAllowListProtection(lua);
+ lua_pop(lua, 1);
+
+ luaLoadLibraries(lua);
+
+ lua_pushcfunction(lua,luaRedisPcall);
+ lua_setglobal(lua, "pcall");
+
+ /* Register the redis commands table and fields */
+ lua_newtable(lua);
+
+ /* redis.call */
+ lua_pushstring(lua,"call");
+ lua_pushcfunction(lua,luaRedisCallCommand);
+ lua_settable(lua,-3);
+
+ /* redis.pcall */
+ lua_pushstring(lua,"pcall");
+ lua_pushcfunction(lua,luaRedisPCallCommand);
+ lua_settable(lua,-3);
+
+ luaRegisterLogFunction(lua);
+
+ luaRegisterVersion(lua);
+
+ /* redis.setresp */
+ lua_pushstring(lua,"setresp");
+ lua_pushcfunction(lua,luaSetResp);
+ lua_settable(lua,-3);
+
+ /* redis.sha1hex */
+ lua_pushstring(lua, "sha1hex");
+ lua_pushcfunction(lua, luaRedisSha1hexCommand);
+ lua_settable(lua, -3);
+
+ /* redis.error_reply and redis.status_reply */
+ lua_pushstring(lua, "error_reply");
+ lua_pushcfunction(lua, luaRedisErrorReplyCommand);
+ lua_settable(lua, -3);
+ lua_pushstring(lua, "status_reply");
+ lua_pushcfunction(lua, luaRedisStatusReplyCommand);
+ lua_settable(lua, -3);
+
+ /* redis.set_repl and associated flags. */
+ lua_pushstring(lua,"set_repl");
+ lua_pushcfunction(lua,luaRedisSetReplCommand);
+ lua_settable(lua,-3);
+
+ lua_pushstring(lua,"REPL_NONE");
+ lua_pushnumber(lua,PROPAGATE_NONE);
+ lua_settable(lua,-3);
+
+ lua_pushstring(lua,"REPL_AOF");
+ lua_pushnumber(lua,PROPAGATE_AOF);
+ lua_settable(lua,-3);
+
+ lua_pushstring(lua,"REPL_SLAVE");
+ lua_pushnumber(lua,PROPAGATE_REPL);
+ lua_settable(lua,-3);
+
+ lua_pushstring(lua,"REPL_REPLICA");
+ lua_pushnumber(lua,PROPAGATE_REPL);
+ lua_settable(lua,-3);
+
+ lua_pushstring(lua,"REPL_ALL");
+ lua_pushnumber(lua,PROPAGATE_AOF|PROPAGATE_REPL);
+ lua_settable(lua,-3);
+
+ /* redis.acl_check_cmd */
+ lua_pushstring(lua,"acl_check_cmd");
+ lua_pushcfunction(lua,luaRedisAclCheckCmdPermissionsCommand);
+ lua_settable(lua,-3);
+
+ /* Finally set the table as 'redis' global var. */
+ lua_setglobal(lua,REDIS_API_NAME);
+
+ /* Replace math.random and math.randomseed with our implementations. */
+ lua_getglobal(lua,"math");
+
+ lua_pushstring(lua,"random");
+ lua_pushcfunction(lua,redis_math_random);
+ lua_settable(lua,-3);
+
+ lua_pushstring(lua,"randomseed");
+ lua_pushcfunction(lua,redis_math_randomseed);
+ lua_settable(lua,-3);
+
+ lua_setglobal(lua,"math");
+}
+
+/* Set an array of Redis String Objects as a Lua array (table) stored into a
+ * global variable. */
+static void luaCreateArray(lua_State *lua, robj **elev, int elec) {
+ int j;
+
+ lua_newtable(lua);
+ for (j = 0; j < elec; j++) {
+ lua_pushlstring(lua,(char*)elev[j]->ptr,sdslen(elev[j]->ptr));
+ lua_rawseti(lua,-2,j+1);
+ }
+}
+
+/* ---------------------------------------------------------------------------
+ * Redis provided math.random
+ * ------------------------------------------------------------------------- */
+
+/* We replace math.random() with our implementation that is not affected
+ * by specific libc random() implementations and will output the same sequence
+ * (for the same seed) in every arch. */
+
+/* The following implementation is the one shipped with Lua itself but with
+ * rand() replaced by redisLrand48(). */
+static int redis_math_random (lua_State *L) {
+ scriptRunCtx* rctx = luaGetFromRegistry(L, REGISTRY_RUN_CTX_NAME);
+ if (!rctx) {
+ return luaL_error(L, "math.random can only be called inside a script invocation");
+ }
+
+ /* the `%' avoids the (rare) case of r==1, and is needed also because on
+ some systems (SunOS!) `rand()' may return a value larger than RAND_MAX */
+ lua_Number r = (lua_Number)(redisLrand48()%REDIS_LRAND48_MAX) /
+ (lua_Number)REDIS_LRAND48_MAX;
+ switch (lua_gettop(L)) { /* check number of arguments */
+ case 0: { /* no arguments */
+ lua_pushnumber(L, r); /* Number between 0 and 1 */
+ break;
+ }
+ case 1: { /* only upper limit */
+ int u = luaL_checkint(L, 1);
+ luaL_argcheck(L, 1<=u, 1, "interval is empty");
+ lua_pushnumber(L, floor(r*u)+1); /* int between 1 and `u' */
+ break;
+ }
+ case 2: { /* lower and upper limits */
+ int l = luaL_checkint(L, 1);
+ int u = luaL_checkint(L, 2);
+ luaL_argcheck(L, l<=u, 2, "interval is empty");
+ lua_pushnumber(L, floor(r*(u-l+1))+l); /* int between `l' and `u' */
+ break;
+ }
+ default: return luaL_error(L, "wrong number of arguments");
+ }
+ return 1;
+}
+
+static int redis_math_randomseed (lua_State *L) {
+ scriptRunCtx* rctx = luaGetFromRegistry(L, REGISTRY_RUN_CTX_NAME);
+ if (!rctx) {
+ return luaL_error(L, "math.randomseed can only be called inside a script invocation");
+ }
+ redisSrand48(luaL_checkint(L, 1));
+ return 0;
+}
+
+/* This is the Lua script "count" hook that we use to detect scripts timeout. */
+static void luaMaskCountHook(lua_State *lua, lua_Debug *ar) {
+ UNUSED(ar);
+ scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME);
+ if (scriptInterrupt(rctx) == SCRIPT_KILL) {
+ serverLog(LL_WARNING,"Lua script killed by user with SCRIPT KILL.");
+
+ /*
+ * Set the hook to invoke all the time so the user
+         * will not be able to catch the error with pcall and invoke
+         * pcall again which will prevent the script from ever been killed
+ */
+ lua_sethook(lua, luaMaskCountHook, LUA_MASKLINE, 0);
+
+ luaPushError(lua,"Script killed by user with SCRIPT KILL...");
+ luaError(lua);
+ }
+}
+
+void luaErrorInformationDiscard(errorInfo *err_info) {
+ if (err_info->msg) sdsfree(err_info->msg);
+ if (err_info->source) sdsfree(err_info->source);
+ if (err_info->line) sdsfree(err_info->line);
+}
+
+void luaExtractErrorInformation(lua_State *lua, errorInfo *err_info) {
+ if (lua_isstring(lua, -1)) {
+ err_info->msg = sdscatfmt(sdsempty(), "ERR %s", lua_tostring(lua, -1));
+ err_info->line = NULL;
+ err_info->source = NULL;
+ err_info->ignore_err_stats_update = 0;
+ }
+
+ lua_getfield(lua, -1, "err");
+ if (lua_isstring(lua, -1)) {
+ err_info->msg = sdsnew(lua_tostring(lua, -1));
+ }
+ lua_pop(lua, 1);
+
+ lua_getfield(lua, -1, "source");
+ if (lua_isstring(lua, -1)) {
+ err_info->source = sdsnew(lua_tostring(lua, -1));
+ }
+ lua_pop(lua, 1);
+
+ lua_getfield(lua, -1, "line");
+ if (lua_isstring(lua, -1)) {
+ err_info->line = sdsnew(lua_tostring(lua, -1));
+ }
+ lua_pop(lua, 1);
+
+ lua_getfield(lua, -1, "ignore_error_stats_update");
+ if (lua_isboolean(lua, -1)) {
+ err_info->ignore_err_stats_update = lua_toboolean(lua, -1);
+ }
+ lua_pop(lua, 1);
+}
+
+void luaCallFunction(scriptRunCtx* run_ctx, lua_State *lua, robj** keys, size_t nkeys, robj** args, size_t nargs, int debug_enabled) {
+ client* c = run_ctx->original_client;
+ int delhook = 0;
+
+ /* We must set it before we set the Lua hook, theoretically the
+ * Lua hook might be called wheneven we run any Lua instruction
+ * such as 'luaSetGlobalArray' and we want the run_ctx to be available
+ * each time the Lua hook is invoked. */
+ luaSaveOnRegistry(lua, REGISTRY_RUN_CTX_NAME, run_ctx);
+
+ if (server.busy_reply_threshold > 0 && !debug_enabled) {
+ lua_sethook(lua,luaMaskCountHook,LUA_MASKCOUNT,100000);
+ delhook = 1;
+ } else if (debug_enabled) {
+ lua_sethook(lua,luaLdbLineHook,LUA_MASKLINE|LUA_MASKCOUNT,100000);
+ delhook = 1;
+ }
+
+ /* Populate the argv and keys table accordingly to the arguments that
+ * EVAL received. */
+ luaCreateArray(lua,keys,nkeys);
+ /* On eval, keys and arguments are globals. */
+ if (run_ctx->flags & SCRIPT_EVAL_MODE){
+ /* open global protection to set KEYS */
+ lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 0);
+ lua_setglobal(lua,"KEYS");
+ lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 1);
+ }
+ luaCreateArray(lua,args,nargs);
+ if (run_ctx->flags & SCRIPT_EVAL_MODE){
+ /* open global protection to set ARGV */
+ lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 0);
+ lua_setglobal(lua,"ARGV");
+ lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 1);
+ }
+
+ /* At this point whether this script was never seen before or if it was
+ * already defined, we can call it.
+ * On eval mode, we have zero arguments and expect a single return value.
+ * In addition the error handler is located on position -2 on the Lua stack.
+ * On function mode, we pass 2 arguments (the keys and args tables),
+ * and the error handler is located on position -4 (stack: error_handler, callback, keys, args) */
+ int err;
+ if (run_ctx->flags & SCRIPT_EVAL_MODE) {
+ err = lua_pcall(lua,0,1,-2);
+ } else {
+ err = lua_pcall(lua,2,1,-4);
+ }
+
+ /* Call the Lua garbage collector from time to time to avoid a
+ * full cycle performed by Lua, which adds too latency.
+ *
+ * The call is performed every LUA_GC_CYCLE_PERIOD executed commands
+ * (and for LUA_GC_CYCLE_PERIOD collection steps) because calling it
+ * for every command uses too much CPU. */
+ #define LUA_GC_CYCLE_PERIOD 50
+ {
+ static long gc_count = 0;
+
+ gc_count++;
+ if (gc_count == LUA_GC_CYCLE_PERIOD) {
+ lua_gc(lua,LUA_GCSTEP,LUA_GC_CYCLE_PERIOD);
+ gc_count = 0;
+ }
+ }
+
+ if (err) {
+ /* Error object is a table of the following format:
+ * {err='<error msg>', source='<source file>', line=<line>}
+ * We can construct the error message from this information */
+ if (!lua_istable(lua, -1)) {
+ const char *msg = "execution failure";
+ if (lua_isstring(lua, -1)) {
+ msg = lua_tostring(lua, -1);
+ }
+ addReplyErrorFormat(c,"Error running script %s, %.100s\n", run_ctx->funcname, msg);
+ } else {
+ errorInfo err_info = {0};
+ sds final_msg = sdsempty();
+ luaExtractErrorInformation(lua, &err_info);
+ final_msg = sdscatfmt(final_msg, "-%s",
+ err_info.msg);
+ if (err_info.line && err_info.source) {
+ final_msg = sdscatfmt(final_msg, " script: %s, on %s:%s.",
+ run_ctx->funcname,
+ err_info.source,
+ err_info.line);
+ }
+ addReplyErrorSdsEx(c, final_msg, err_info.ignore_err_stats_update? ERR_REPLY_FLAG_NO_STATS_UPDATE : 0);
+ luaErrorInformationDiscard(&err_info);
+ }
+ lua_pop(lua,1); /* Consume the Lua error */
+ } else {
+ /* On success convert the Lua return value into Redis protocol, and
+ * send it to * the client. */
+ luaReplyToRedisReply(c, run_ctx->c, lua); /* Convert and consume the reply. */
+ }
+
+ /* Perform some cleanup that we need to do both on error and success. */
+ if (delhook) lua_sethook(lua,NULL,0,0); /* Disable hook */
+
+ /* remove run_ctx from registry, its only applicable for the current script. */
+ luaSaveOnRegistry(lua, REGISTRY_RUN_CTX_NAME, NULL);
+}
+
+unsigned long luaMemory(lua_State *lua) {
+ return lua_gc(lua, LUA_GCCOUNT, 0) * 1024LL;
+}