From 76d187e5682f5a4b980f6d9859b6fdd0502e7fc0 Mon Sep 17 00:00:00 2001 From: Christopher Ramey Date: Mon, 21 May 2012 17:09:41 +0000 Subject: [PATCH] Switched to mmap for loading files. Added POST variable handling. Added sandboxed loadstring, loadfile and dofile functions for Lua. Fixed bug where errors generated during request parsing may cause memory leaks on certain platforms. Fixed bug where non-string errors generated by Lua would cause a segmentation fault. Corrected various typos and unclear code comments. --- LICENCE | 2 +- Makefile | 1 + README.md | 2 +- TODO | 10 +-- lua-fastcgi.lua | 2 +- src/lfuncs.c | 154 ++++++++++++++++++++++++++++++++----- src/lfuncs.h | 10 +++ src/lua-fastcgi.c | 48 +++++++----- src/lua.c | 190 +++++++++++++++++++++++++++++++--------------- src/lua.h | 19 ++++- 10 files changed, 328 insertions(+), 110 deletions(-) diff --git a/LICENCE b/LICENCE index e393d1b..ca2da75 100644 --- a/LICENCE +++ b/LICENCE @@ -9,7 +9,7 @@ are met: this list of conditions and the following disclaimer. 2. 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. + and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE diff --git a/Makefile b/Makefile index 3e4920e..8823add 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ LDFLAGS=-O2 -Wl,-Bstatic -lfcgi -llua5.1 -Wl,-Bdynamic -lm -lpthread all: lua-fastcgi debug: CFLAGS+=-g -DDEBUG +debug: LDFLAGS+=-lrt debug: lua-fastcgi lua-fastcgi: src/lua-fastcgi.o src/lfuncs.o src/lua.o src/config.o diff --git a/README.md b/README.md index 00ee14f..e1c32e3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ lua-fastcgi lua-fastcgi is a sandboxed Lua backend for FastCGI. That is, you can write Lua scripts that serve up web pages. Options exist in lua-fastcgi.lua to configure a fixed amount of memory, cpu usage, and output bytes -for each request. While sandboxed, lua-fastcgi supports a limit set +for each request. While sandboxed, lua-fastcgi supports a limited set of functions. If sandboxing is disabled, lua-fastcgi loads the standard libraries and users may load modules as needed. diff --git a/TODO b/TODO index 92f45c3..d66b614 100644 --- a/TODO +++ b/TODO @@ -1,17 +1,17 @@ High Priority ------------- -POST variable parsing Basic sandboxed database support (e.g. GET/PUT) -Sandboxed dofile() / loadfile() / loadstring() +Function for reading files into string Medium Priority --------------- -File Upload Support -Database error logging -Consider switching to mmap() for file reads +File upload support +Logging of errors to database Low Priority ------------ Database file adapter +Session scoped variables +Application scoped variables diff --git a/lua-fastcgi.lua b/lua-fastcgi.lua index 37809c1..e2ba185 100644 --- a/lua-fastcgi.lua +++ b/lua-fastcgi.lua @@ -30,7 +30,7 @@ return { -- Limit page output to x bytes or 0 for unlimited -- Default: 65536 - output_max = 0, + output_max = 65536, -- Default content type returned in header content_type = "text/html; charset=iso-8859-1" diff --git a/src/lfuncs.c b/src/lfuncs.c index bbe8f80..0f935c7 100644 --- a/src/lfuncs.c +++ b/src/lfuncs.c @@ -17,9 +17,9 @@ static int LF_pprint(lua_State *l, int cr) int args = lua_gettop(l); // Fetch the response - lua_pushstring(l, "RESPONSE"); + lua_pushstring(l, "STATE"); lua_rawget(l, LUA_REGISTRYINDEX); - LF_response *response = lua_touserdata(l, args+1); + LF_state *state = lua_touserdata(l, args+1); lua_pop(l, 1); // fetch limits @@ -28,9 +28,8 @@ static int LF_pprint(lua_State *l, int cr) size_t *limit = lua_touserdata(l, args+1); lua_pop(l, 1); - // If the response isn't committed, send the header - if(!response->committed){ + if(!state->committed){ lua_getglobal(l, "HEADER"); if(!lua_istable(l, args+1)){ luaL_error(l, "Invalid HEADER (Not table)."); } @@ -51,10 +50,10 @@ static int LF_pprint(lua_State *l, int cr) *limit -= (len+10); } - FCGX_PutStr("Status: ", 8, response->out); - FCGX_PutStr(str, len, response->out); - FCGX_PutStr("\r\n", 2, response->out); - response->committed = 1; + FCGX_PutStr("Status: ", 8, state->response); + FCGX_PutStr(str, len, state->response); + FCGX_PutStr("\r\n", 2, state->response); + state->committed = 1; } lua_pop(l, 1); // Pop the status @@ -82,12 +81,12 @@ static int LF_pprint(lua_State *l, int cr) *limit -= (vallen+keylen+4); } - FCGX_PutStr(key, keylen, response->out); - FCGX_PutStr(": ", 2, response->out); - FCGX_PutStr(val, vallen, response->out); - FCGX_PutStr("\r\n", 2, response->out); + FCGX_PutStr(key, keylen, state->response); + FCGX_PutStr(": ", 2, state->response); + FCGX_PutStr(val, vallen, state->response); + FCGX_PutStr("\r\n", 2, state->response); - response->committed = 1; + state->committed = 1; lua_pop(l, 1); // Clear the last value out } lua_pop(l, 1); // Clear the table out @@ -97,8 +96,8 @@ static int LF_pprint(lua_State *l, int cr) *limit -= 2; } - FCGX_PutS("\r\n", response->out); - response->committed = 1; + FCGX_PutS("\r\n", state->response); + state->committed = 1; } size_t strlen; @@ -114,7 +113,7 @@ static int LF_pprint(lua_State *l, int cr) *limit -= strlen; } - FCGX_PutStr(str, strlen, response->out); + FCGX_PutStr(str, strlen, state->response); break; default: /* Ignore other types */ break; @@ -127,7 +126,7 @@ static int LF_pprint(lua_State *l, int cr) (*limit)--; } - FCGX_PutChar('\n', response->out); + FCGX_PutChar('\n', state->response); } return 0; } @@ -135,3 +134,124 @@ static int LF_pprint(lua_State *l, int cr) int LF_print(lua_State *l){ return LF_pprint(l, 1); } int LF_write(lua_State *l){ return LF_pprint(l, 0); } + + +int LF_loadstring(lua_State *l) +{ + size_t sz; + const char *s = luaL_checklstring(l, 1, &sz); + + if(sz > 3 && memcmp(s, LUA_SIGNATURE, 4) == 0){ + lua_pushnil(l); + lua_pushstring(l, "Compiled bytecode not supported."); + return 2; + } + + if(luaL_loadbuffer(l, s, sz, luaL_optstring(l, 2, s)) == 0){ + return 1; + } else { + lua_pushnil(l); + lua_insert(l, -2); + return 2; + } +} + + +int LF_loadfile(lua_State *l) +{ + size_t sz; + const char *spath = luaL_checklstring(l, 1, &sz); + + lua_pushstring(l, "DOCUMENT_ROOT"); + lua_rawget(l, LUA_REGISTRYINDEX); + char *document_root = lua_touserdata(l, -1); + lua_pop(l, 1); + + if(document_root == NULL){ + lua_pushnil(l); + lua_pushstring(l, "DOCUMENT_ROOT not defined."); + return 2; + } + + size_t dz = strlen(document_root); + + if(dz == 0){ + lua_pushnil(l); + lua_pushstring(l, "DOCUMENT_ROOT empty."); + return 2; + } + + size_t hz = dz + sz; + if((hz + 2) > 4096){ + lua_pushnil(l); + lua_pushstring(l, "Path too large."); + return 2; + } + + char hpath[4096]; + memcpy(&hpath[0], document_root, dz); + if(hpath[dz-1] != '/' && spath[0] != '/'){ hpath[dz] = '/'; } + memcpy(&hpath[dz+1], spath, sz); + hpath[hz+1] = 0; + + char rpath[4096]; + char *ptr = realpath(hpath, rpath); + if(ptr == NULL || memcmp(document_root, rpath, dz) != 0){ + lua_pushnil(l); + lua_pushstring(l, "Invalid path."); + return 2; + } + + switch(LF_fileload(l, &spath[0], &hpath[0])){ + case 0: + return 1; + break; + + case LF_ERRACCESS: + lua_pushnil(l); + lua_pushstring(l, "Access denied."); + break; + + case LF_ERRMEMORY: + lua_pushnil(l); + lua_pushstring(l, "Not enough memory."); + break; + + case LF_ERRNOTFOUND: + lua_pushnil(l); + lua_pushstring(l, "No such file or directory."); + break; + + case LF_ERRSYNTAX: + lua_pushnil(l); + lua_insert(l, -2); + break; + + case LF_ERRBYTECODE: + lua_pushnil(l); + lua_pushstring(l, "Compiled bytecode not supported."); + break; + + case LF_ERRNOPATH: + case LF_ERRNONAME: + lua_pushnil(l); + lua_pushstring(l, "Invalid path."); + break; + } + + return 2; +} + + +int LF_dofile(lua_State *l) +{ + int r = LF_loadfile(l); + if(r == 1 && lua_isfunction(l, -1)){ + lua_call(l, 0, LUA_MULTRET); + return lua_gettop(l) - 1; + } else { + lua_error(l); + } + + return 0; +} diff --git a/src/lfuncs.h b/src/lfuncs.h index c587e41..6229e9f 100644 --- a/src/lfuncs.h +++ b/src/lfuncs.h @@ -1,4 +1,14 @@ // Writes FCGI output followed by a carriage return int LF_print(lua_State *); + // Writes FCGI output without a carriage return int LF_write(lua_State *); + +// loadstring() function with anti-bytecode security measures +int LF_loadstring(lua_State *); + +// loadfile() function with sandboxing security measures +int LF_loadfile(lua_State *); + +// dofile() function with sandboxing security measures +int LF_dofile(lua_State *); diff --git a/src/lua-fastcgi.c b/src/lua-fastcgi.c index f3533a1..91907fe 100644 --- a/src/lua-fastcgi.c +++ b/src/lua-fastcgi.c @@ -25,12 +25,12 @@ static char *http_status_strings[] = { #define senderror(status_code,error_string) \ - if(!response.committed){ \ + if(!state.committed){ \ FCGX_FPrintF(request.out, "Status: %d %s\r\n", status_code, http_status_strings[status_code]); \ FCGX_FPrintF(request.out, "Content-Type: %s\r\n\r\n", config->content_type); \ - response.committed = 1; \ + state.committed = 1; \ } \ - FCGX_PutS(error_string, response.out); + FCGX_PutS(error_string, state.response); #ifdef DEBUG @@ -64,7 +64,7 @@ void *thread_run(void *arg) LF_params *params = arg; LF_config *config = params->config; LF_limits *limits = LF_newlimits(); - LF_response response; + LF_state state; lua_State *l; FCGX_Request request; @@ -86,31 +86,43 @@ void *thread_run(void *arg) #ifdef DEBUG printvars(&request); + struct timespec rstart, rend; + clock_gettime(CLOCK_MONOTONIC, &rstart); + #endif + + LF_parserequest(l, &request, &state); + + #ifdef DEBUG + clock_gettime(CLOCK_MONOTONIC, &rend); + // Assumes the request returns in less than a second (which it should) + printf("Request parsed in %luns\n", (rend.tv_nsec-rstart.tv_nsec)); #endif - LF_parserequest(l, &request, &response); LF_enablelimits(l, limits); - switch(LF_loadfile(l)){ + switch(LF_loadscript(l)){ case 0: if(lua_pcall(l, 0, 0, 0)){ - senderror(500, lua_tostring(l, -1)); - } else if(!response.committed){ + if(lua_isstring(l, -1)){ + senderror(500, lua_tostring(l, -1)); + } else { + senderror(500, "unspecified lua error"); + } + } else if(!state.committed){ senderror(200, ""); } break; - case EACCES: - senderror(403, lua_tostring(l, -1)); - break; - - case ENOENT: - senderror(404, lua_tostring(l, -1)); - break; - - default: - senderror(500, lua_tostring(l, -1)); + case LF_ERRACCESS: senderror(403, "access denied"); break; + case LF_ERRMEMORY: senderror(500, "not enough memory"); break; + case LF_ERRNOTFOUND: + printf("404\n"); + senderror(404, "no such file or directory"); break; + case LF_ERRSYNTAX: senderror(500, lua_tostring(l, -1)); break; + case LF_ERRBYTECODE: senderror(403, "compiled bytecode not supported"); break; + case LF_ERRNOPATH: senderror(500, "SCRIPT_FILENAME not provided"); break; + case LF_ERRNONAME: senderror(500, "SCRIPT_NAME not provided"); break; } FCGX_Finish_r(&request); diff --git a/src/lua.c b/src/lua.c index 19d7180..4ece42f 100644 --- a/src/lua.c +++ b/src/lua.c @@ -1,10 +1,13 @@ #include +#include #include #include #include #include #include #include +#include +#include #include #include @@ -17,14 +20,6 @@ #include "lua.h" #include "lfuncs.h" -#define LF_BUFFERSIZE 4096 - -typedef struct { - int fd; - char buffer[LF_BUFFERSIZE]; - size_t total; -} LF_loaderdata; - #ifdef DEBUG void LF_printstack(lua_State *l) @@ -119,7 +114,6 @@ void LF_setlimits(LF_limits *limits, size_t memory, size_t output, uint32_t cpu_ } - void LF_enablelimits(lua_State *l, LF_limits *limits) { if(limits->cpu.tv_usec > 0 || limits->cpu.tv_sec > 0){ @@ -142,7 +136,13 @@ void LF_enablelimits(lua_State *l, LF_limits *limits) lua_rawset(l, LUA_REGISTRYINDEX); } - if(limits->memory){ lua_setallocf(l, &LF_limit_alloc, &limits->memory); } + if(limits->memory){ + lua_pushstring(l, "MEMORY_LIMIT"); + lua_pushlightuserdata(l, &limits->memory); + lua_rawset(l, LUA_REGISTRYINDEX); + + lua_setallocf(l, &LF_limit_alloc, &limits->memory); + } } @@ -174,13 +174,16 @@ lua_State *LF_newstate(int sandbox, char *content_type) lua_call(l, 1, 0); // Nil out unsafe functions/objects - LF_nilglobal(l, "dofile"); LF_nilglobal(l, "load"); - LF_nilglobal(l, "loadfile"); LF_nilglobal(l, "xpcall"); LF_nilglobal(l, "pcall"); LF_nilglobal(l, "module"); LF_nilglobal(l, "require"); + + // Override unsafe functions + lua_register(l, "loadstring", &LF_loadstring); + lua_register(l, "loadfile", &LF_loadfile); + lua_register(l, "dofile", &LF_dofile); } // Register the print function @@ -202,7 +205,7 @@ lua_State *LF_newstate(int sandbox, char *content_type) // Set GET variables -static void LF_parsequerystring(lua_State *l, char *query_string) +static void LF_parsequerystring(lua_State *l, char *query_string, char *table) { lua_newtable(l); @@ -262,7 +265,7 @@ static void LF_parsequerystring(lua_State *l, char *query_string) if(lua_gettop(l) == (stack+2)){ lua_rawset(l, stack); } // Finally, set the table - lua_setglobal(l, "GET"); + lua_setglobal(l, table); return; break; @@ -275,8 +278,17 @@ static void LF_parsequerystring(lua_State *l, char *query_string) // Parses fastcgi request -void LF_parserequest(lua_State *l, FCGX_Request *request, LF_response *response) +void LF_parserequest(lua_State *l, FCGX_Request *request, LF_state *state) { + uintmax_t content_length = 0; + char *content_type = NULL; + + state->committed = 0; + state->response = request->out; + lua_pushstring(l, "STATE"); + lua_pushlightuserdata(l, state); + lua_rawset(l, LUA_REGISTRYINDEX); + lua_newtable(l); for(char **p = request->envp; *p; ++p){ char *vptr = strchr(*p, '='); @@ -285,77 +297,129 @@ void LF_parserequest(lua_State *l, FCGX_Request *request, LF_response *response) lua_pushlstring(l, *p, keylen); // Push Key lua_pushstring(l, (vptr+1)); // Push Value lua_rawset(l, 1); // Set key/value into table + + switch(keylen){ + case 11: + if(memcmp(*p, "SCRIPT_NAME", 11) == 0){ + lua_pushstring(l, "SCRIPT_NAME"); + lua_pushlightuserdata(l, (vptr+1)); + lua_rawset(l, LUA_REGISTRYINDEX); + } + break; - if(keylen == 12 && memcmp(*p, "QUERY_STRING", 12) == 0){ - LF_parsequerystring(l, (vptr+1)); + case 12: + if(memcmp(*p, "QUERY_STRING", 12) == 0){ + LF_parsequerystring(l, (vptr+1), "GET"); + } if(memcmp(*p, "CONTENT_TYPE", 12) == 0){ + content_type = (vptr+1); + } + break; + + case 13: + if(memcmp(*p, "DOCUMENT_ROOT", 13) == 0){ + lua_pushstring(l, "DOCUMENT_ROOT"); + lua_pushlightuserdata(l, (vptr+1)); + lua_rawset(l, LUA_REGISTRYINDEX); + } + break; + + case 14: + if(memcmp(*p, "CONTENT_LENGTH", 14) == 0){ + content_length = strtoumax((vptr+1), NULL, 10); + } + break; + + case 15: + if(memcmp(*p, "SCRIPT_FILENAME", 15) == 0){ + lua_pushstring(l, "SCRIPT_FILENAME"); + lua_pushlightuserdata(l, (vptr+1)); + lua_rawset(l, LUA_REGISTRYINDEX); + } + break; } } lua_setglobal(l, "REQUEST"); - response->committed = 0; - response->out = request->out; - lua_pushstring(l, "RESPONSE"); - lua_pushlightuserdata(l, response); - lua_rawset(l, LUA_REGISTRYINDEX); + if(content_length > 0 && content_type != NULL && memcmp(content_type, "application/x-www-form-urlencoded", 33) == 0){ + char *content = lua_newuserdata(l, content_length+1); + int r = FCGX_GetStr( + content, (content_length > INT_MAX ? INT_MAX : content_length), + request->in + ); + *(content + r) = 0; // Add NUL byte at end for proper string + LF_parsequerystring(l, content, "POST"); + lua_pop(l, 1); + } } -static const char *LF_filereader(lua_State *l, void *data, size_t *size) +// Load script by name and path +int LF_fileload(lua_State *l, const char *name, char *scriptpath) { - LF_loaderdata *ld = data; + char *script = NULL; + int fd = -1, r = 0; + struct stat sb; + + if(scriptpath == NULL){ return LF_ERRNOPATH; } + if(name == NULL){ return LF_ERRNONAME; } - *size = read(ld->fd, ld->buffer, LF_BUFFERSIZE); + // Generate a string with an '=' followed by the script name + // this ensures lua will generation a reasonable error + size_t namelen = strlen(name); + char scriptname[namelen+2]; + scriptname[0] = '='; + memcpy(&scriptname[1], name, namelen+1); - if(ld->total == 0 && *size > 3){ - if(memcmp(ld->buffer, LUA_SIGNATURE, 4) == 0){ - luaL_error(l, "Compiled bytecode not supported."); + if((fd = open(scriptpath, O_RDONLY)) == -1){ goto errorL; } + + if(fstat(fd, &sb) == -1){ goto errorL; } + + if((script = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0)) == NULL){ + goto errorL; + } + + if(madvise(script, sb.st_size, MADV_SEQUENTIAL) == -1){ goto errorL; } + + if(sb.st_size > 3 && memcmp(script, LUA_SIGNATURE, 4) == 0){ + r = LF_ERRBYTECODE; + } else { + switch(luaL_loadbuffer(l, script, sb.st_size, scriptname)){ + case LUA_ERRSYNTAX: r = LF_ERRSYNTAX; break; + case LUA_ERRMEM: r = LF_ERRMEMORY; break; } } - switch(*size){ - case 0: return NULL; - case -1: luaL_error(l, strerror(errno)); - default: - ld->total += *size; - return ld->buffer; + if(script != NULL){ munmap(script, sb.st_size); } + if(fd != -1){ close(fd); } + return r; + + errorL: + if(script != NULL){ munmap(script, sb.st_size); } + if(fd != -1){ close(fd); } + switch(errno){ + case EACCES: return r = LF_ERRACCESS; + case ENOENT: return r = LF_ERRNOTFOUND; + case ENOMEM: return r = LF_ERRMEMORY; + default: return r = LF_ERRANY; } + return r; } -// Loads a lua file into a state -int LF_loadfile(lua_State *l) +// Loads script specified in registryindex into lua state +int LF_loadscript(lua_State *l) { - lua_getglobal(l, "REQUEST"); - - int stack = lua_gettop(l); - lua_pushstring(l, "SCRIPT_FILENAME"); - lua_rawget(l, stack); - const char *path = lua_tostring(l, stack+1); + lua_rawget(l, LUA_REGISTRYINDEX); + char *scriptpath = lua_touserdata(l, 1); + lua_pop(l, 1); lua_pushstring(l, "SCRIPT_NAME"); - lua_rawget(l, stack); - const char *name = lua_tostring(l, stack+2); - - LF_loaderdata ld; - ld.total = 0; - ld.fd = open(path, O_RDONLY); - if(ld.fd == -1){ - lua_pushstring(l, strerror(errno)); - return errno; - } - - // Generate a string with an '=' followed by the script name - // this ensures lua will generation a reasonable error - size_t len = strlen(name) + 1; - char scriptname[len + 1]; - scriptname[0] = '='; - memcpy(&scriptname[1], name, len); - - int r = lua_load(l, &LF_filereader, &ld, scriptname); + lua_rawget(l, LUA_REGISTRYINDEX); + char *name = lua_touserdata(l, 1); + lua_pop(l, 1); - close(ld.fd); - return (r == 0 ? 0 : ENOMSG); + return LF_fileload(l, name, scriptpath); } diff --git a/src/lua.h b/src/lua.h index 6216443..b29698b 100644 --- a/src/lua.h +++ b/src/lua.h @@ -1,7 +1,17 @@ +#define LF_ERRNONE 0 +#define LF_ERRANY 1 +#define LF_ERRACCESS 2 +#define LF_ERRMEMORY 3 +#define LF_ERRNOTFOUND 4 +#define LF_ERRSYNTAX 5 +#define LF_ERRBYTECODE 6 +#define LF_ERRNOPATH 7 +#define LF_ERRNONAME 8 + typedef struct { + FCGX_Stream *response; int committed; - FCGX_Stream *out; -} LF_response; +} LF_state; typedef struct { size_t memory; @@ -14,7 +24,8 @@ lua_State *LF_newstate(int, char *); LF_limits *LF_newlimits(); void LF_setlimits(LF_limits *, size_t, size_t, uint32_t, uint32_t); void LF_enablelimits(lua_State *, LF_limits *); -void LF_parserequest(lua_State *l, FCGX_Request *, LF_response *); +void LF_parserequest(lua_State *l, FCGX_Request *, LF_state *); void LF_emptystack(lua_State *); -int LF_loadfile(lua_State *); +int LF_fileload(lua_State *, const char *, char *); +int LF_loadscript(lua_State *); void LF_closestate(lua_State *);