Hide shell functions from _G using upvalues on the main chunk

Store __command, __interactive, __getenv, __setenv as upvalues
populated by lua_load() from the registry, keeping them invisible
to user code while accessible to the parser's codegen.
This commit is contained in:
Cormac Shannon
2026-03-12 22:46:56 +00:00
parent 34bfabccbd
commit f88b17959f
8 changed files with 134 additions and 77 deletions

22
lapi.c
View File

@@ -29,6 +29,7 @@
#include "ltm.h"
#include "lundump.h"
#include "lvm.h"
#include "lcmd.h"
@@ -1135,6 +1136,27 @@ LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data,
setobj(L, f->upvals[0]->v.p, &gt);
luaC_barrier(L, f->upvals[0], &gt);
}
if (f->nupvalues >= LUSH_NUM_UPVALS) { /* has lush upvalues? */
/* populate shell function upvalues from registry */
Table *regt = hvalue(&G(L)->l_registry);
TValue lushtv;
lu_byte tag = luaH_getint(regt, LUA_RIDX_LUSH, &lushtv);
if (novariant(tag) == LUA_TTABLE) {
static const char *const fields[] = {
"command", "interactive", "getenv", "setenv"
};
Table *lusht = hvalue(&lushtv);
int i;
for (i = 0; i < 4; i++) {
TValue val;
TString *key = luaS_new(L, fields[i]);
if (luaH_getstr(lusht, key, &val) != LUA_TNIL) {
setobj(L, f->upvals[i + 1]->v.p, &val);
luaC_barrier(L, f->upvals[i + 1], &val);
}
}
}
}
}
lua_unlock(L);
return APIstatus(status);

View File

@@ -155,5 +155,9 @@ void luaopen_builtins (lua_State *L) {
lua_setfield(L, -2, "exec");
lua_pushcfunction(L, builtin_umask);
lua_setfield(L, -2, "umask");
lua_setglobal(L, "__builtins");
/* Store as __lush.builtins in registry */
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_LUSH);
lua_insert(L, -2);
lua_setfield(L, -2, "builtins");
lua_pop(L, 1);
}

47
lcmd.c
View File

@@ -874,15 +874,21 @@ static int exec_pipeline (lua_State *L, char **stages, int nstages,
*/
static int try_builtin (lua_State *L, ParsedArgs *pa) {
int i;
if (lua_getglobal(L, "__builtins") != LUA_TTABLE) {
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_LUSH);
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
return 0;
}
if (lua_getfield(L, -1, pa->argv[0]) != LUA_TFUNCTION) {
if (lua_getfield(L, -1, "builtins") != LUA_TTABLE) {
lua_pop(L, 2);
return 0;
}
lua_remove(L, -2); /* remove __builtins, keep function */
if (lua_getfield(L, -1, pa->argv[0]) != LUA_TFUNCTION) {
lua_pop(L, 3);
return 0;
}
lua_remove(L, -2); /* remove builtins table */
lua_remove(L, -2); /* remove lush table */
for (i = 0; i < pa->argc; i++)
lua_pushstring(L, pa->argv[i]);
lua_call(L, pa->argc, 1);
@@ -890,9 +896,9 @@ static int try_builtin (lua_State *L, ParsedArgs *pa) {
}
/* ===== luaB_command ===== */
/* ===== lushCmd_command ===== */
int luaB_command (lua_State *L) {
int lushCmd_command (lua_State *L) {
const char *cmd = luaL_checkstring(L, 1);
char *stages[MAX_PIPELINE_STAGES];
int nstages;
@@ -1033,9 +1039,9 @@ int luaB_command (lua_State *L) {
}
/* ===== luaB_interactive ===== */
/* ===== lushCmd_interactive ===== */
int luaB_interactive (lua_State *L) {
int lushCmd_interactive (lua_State *L) {
const char *cmd = luaL_checkstring(L, 1);
char *stages[MAX_PIPELINE_STAGES];
int nstages;
@@ -1142,3 +1148,30 @@ int luaB_interactive (lua_State *L) {
return 0; /* void — no Lua return values */
}
/* ===== lushCmd_getenv ===== */
int lushCmd_getenv (lua_State *L) {
const char *name = luaL_checkstring(L, 1);
const char *val = getenv(name);
if (val == NULL)
lua_pushnil(L);
else
lua_pushstring(L, val);
return 1;
}
/* ===== lushCmd_setenv ===== */
int lushCmd_setenv (lua_State *L) {
const char *name = luaL_checkstring(L, 1);
if (lua_isnoneornil(L, 2))
unsetenv(name);
else {
const char *val = luaL_tolstring(L, 2, NULL);
setenv(name, val, 1);
}
return 0;
}

14
lcmd.h
View File

@@ -9,7 +9,17 @@
#include "lua.h"
int luaB_command (lua_State *L);
int luaB_interactive (lua_State *L);
/* Upvalue indices for shell functions on the main chunk.
** Index 0 is _ENV (standard). Indices 1-4 are lush shell functions. */
#define LUSH_UPVAL_COMMAND 1
#define LUSH_UPVAL_INTERACTIVE 2
#define LUSH_UPVAL_GETENV 3
#define LUSH_UPVAL_SETENV 4
#define LUSH_NUM_UPVALS 5 /* total: _ENV(0) + 4 shell functions */
int lushCmd_command (lua_State *L);
int lushCmd_interactive (lua_State *L);
int lushCmd_getenv (lua_State *L);
int lushCmd_setenv (lua_State *L);
#endif

44
linit.c
View File

@@ -43,41 +43,21 @@ static const luaL_Reg stdlibs[] = {
};
static int luaB_getenv (lua_State *L) {
const char *name = luaL_checkstring(L, 1);
const char *val = getenv(name);
if (val == NULL)
lua_pushnil(L);
else
lua_pushstring(L, val);
return 1;
}
static int luaB_setenv (lua_State *L) {
const char *name = luaL_checkstring(L, 1);
if (lua_isnoneornil(L, 2))
unsetenv(name);
else {
const char *val = luaL_tolstring(L, 2, NULL);
setenv(name, val, 1);
}
return 0;
}
/*
** Register shell built-in globals (called after standard libraries).
** Store shell functions in a registry table at LUA_RIDX_LUSH.
** These are populated as upvalues on the main chunk by lua_load().
*/
static void opencommand (lua_State *L) {
lua_pushcfunction(L, luaB_command);
lua_setglobal(L, "__command");
lua_pushcfunction(L, luaB_interactive);
lua_setglobal(L, "__interactive");
lua_pushcfunction(L, luaB_getenv);
lua_setglobal(L, "__getenv");
lua_pushcfunction(L, luaB_setenv);
lua_setglobal(L, "__setenv");
lua_createtable(L, 0, 5);
lua_pushcfunction(L, lushCmd_command);
lua_setfield(L, -2, "command");
lua_pushcfunction(L, lushCmd_interactive);
lua_setfield(L, -2, "interactive");
lua_pushcfunction(L, lushCmd_getenv);
lua_setfield(L, -2, "getenv");
lua_pushcfunction(L, lushCmd_setenv);
lua_setfield(L, -2, "setenv");
lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_LUSH);
}

View File

@@ -27,6 +27,7 @@
#include "lstate.h"
#include "lstring.h"
#include "ltable.h"
#include "lcmd.h"
@@ -499,6 +500,19 @@ static void singlevaraux (FuncState *fs, TString *n, expdesc *var, int base) {
}
/*
** Look up a lush shell function (e.g. __command) that is registered
** as an upvalue on the main chunk. singlevaraux will find it directly
** as VUPVAL, emitting OP_GETUPVAL instead of OP_GETTABUP.
*/
static void buildlushvar (LexState *ls, TString *varname, expdesc *var) {
FuncState *fs = ls->fs;
init_exp(var, VGLOBAL, -1);
singlevaraux(fs, varname, var, 1);
lua_assert(var->k == VUPVAL); /* must be found as upvalue */
}
static void buildglobal (LexState *ls, TString *varname, expdesc *var) {
FuncState *fs = ls->fs;
expdesc key;
@@ -552,8 +566,8 @@ static void codecommand (LexState *ls, expdesc *v, expdesc *cmdstr) {
expdesc func;
TString *cmdname = luaX_newstring(ls, "__command", 9);
line = ls->linenumber;
/* look up __command as _ENV["__command"] */
buildglobal(ls, cmdname, &func);
/* look up __command as upvalue */
buildlushvar(ls, cmdname, &func);
luaK_exp2nextreg(fs, &func);
base = func.u.info;
/* push the command string argument */
@@ -1243,7 +1257,7 @@ static void commandexp (LexState *ls, expdesc *v) {
TString *cmdname = luaX_newstring(ls, "__command", 9);
TString *tsname = luaX_newstring(ls, "tostring", 8);
/* load __command function first so it occupies the base register */
buildglobal(ls, cmdname, &func);
buildlushvar(ls, cmdname, &func);
luaK_exp2nextreg(fs, &func);
base = func.u.info;
/* load the first fragment */
@@ -1308,7 +1322,7 @@ static void codeenvget (LexState *ls, expdesc *v, TString *name) {
expdesc func, arg;
TString *fname = luaX_newstring(ls, "__getenv", 8);
line = ls->linenumber;
buildglobal(ls, fname, &func);
buildlushvar(ls, fname, &func);
luaK_exp2nextreg(fs, &func);
base = func.u.info;
codestring(&arg, name);
@@ -2193,7 +2207,7 @@ static void envstat (LexState *ls) {
luaX_next(ls); /* skip TK_ENVVAR */
checknext(ls, '=');
fname = luaX_newstring(ls, "__setenv", 8);
buildglobal(ls, fname, &func);
buildlushvar(ls, fname, &func);
luaK_exp2nextreg(fs, &func);
base = func.u.info;
codestring(&arg, name);
@@ -2219,7 +2233,7 @@ static void interactivestat (LexState *ls) {
expdesc func, cmdstr, v;
TString *fname = luaX_newstring(ls, "__interactive", 13);
line = ls->linenumber;
buildglobal(ls, fname, &func);
buildlushvar(ls, fname, &func);
luaK_exp2nextreg(fs, &func);
base = func.u.info;
codestring(&cmdstr, ls->t.seminfo.ts);
@@ -2236,7 +2250,7 @@ static void interactivestat (LexState *ls) {
int line = ls->linenumber;
TString *fname = luaX_newstring(ls, "__interactive", 13);
TString *tsname = luaX_newstring(ls, "tostring", 8);
buildglobal(ls, fname, &func);
buildlushvar(ls, fname, &func);
luaK_exp2nextreg(fs, &func);
base = func.u.info;
/* load the first fragment */
@@ -2396,8 +2410,12 @@ static void statement (LexState *ls) {
** upvalue named LUA_ENV
*/
static void mainfunc (LexState *ls, FuncState *fs) {
static const char *const lush_upval_names[] = {
"__command", "__interactive", "__getenv", "__setenv"
};
BlockCnt bl;
Upvaldesc *env;
int i;
open_func(ls, fs, &bl);
setvararg(fs, PF_ISVARARG); /* main function is always vararg */
env = allocupvalue(fs); /* ...set environment upvalue */
@@ -2406,6 +2424,15 @@ static void mainfunc (LexState *ls, FuncState *fs) {
env->kind = VDKREG;
env->name = ls->envn;
luaC_objbarrier(ls->L, fs->f, env->name);
/* allocate upvalues for lush shell functions */
for (i = 0; i < 4; i++) {
Upvaldesc *uv = allocupvalue(fs);
uv->instack = 1;
uv->idx = 0;
uv->kind = VDKREG;
uv->name = luaS_new(ls->L, lush_upval_names[i]);
luaC_objbarrier(ls->L, fs->f, uv->name);
}
luaX_next(ls); /* read first token */
statlist(ls); /* parse main body */
check(ls, TK_EOS);
@@ -2417,7 +2444,7 @@ LClosure *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff,
Dyndata *dyd, const char *name, int firstchar) {
LexState lexstate;
FuncState funcstate;
LClosure *cl = luaF_newLclosure(L, 1); /* create main closure */
LClosure *cl = luaF_newLclosure(L, LUSH_NUM_UPVALS); /* create main closure */
setclLvalue2s(L, L->top.p, cl); /* anchor it (to avoid being collected) */
luaD_inctop(L);
lexstate.h = luaH_new(L); /* create table for scanner */
@@ -2432,7 +2459,7 @@ LClosure *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff,
dyd->actvar.n = dyd->gt.n = dyd->label.n = 0;
luaX_setinput(L, &lexstate, z, funcstate.f->source, firstchar);
mainfunc(&lexstate, &funcstate);
lua_assert(!funcstate.prev && funcstate.nups == 1 && !lexstate.fs);
lua_assert(!funcstate.prev && funcstate.nups == LUSH_NUM_UPVALS && !lexstate.fs);
/* all scopes should be correctly finished */
lua_assert(dyd->actvar.n == 0 && dyd->gt.n == 0 && dyd->label.n == 0);
L->top.p--; /* remove scanner's table */

3
lua.h
View File

@@ -83,7 +83,8 @@ typedef struct lua_State lua_State;
/* index 1 is reserved for the reference mechanism */
#define LUA_RIDX_GLOBALS 2
#define LUA_RIDX_MAINTHREAD 3
#define LUA_RIDX_LAST 3
#define LUA_RIDX_LUSH 4
#define LUA_RIDX_LAST LUA_RIDX_LUSH
/* type of numbers in Lua */

View File

@@ -3,13 +3,10 @@
print "testing shell builtins"
-- === __builtins table ===
-- === __builtins is NOT in _G (internal, stored in registry) ===
do
assert(type(__builtins) == "table")
assert(type(__builtins.cd) == "function")
assert(type(__builtins.exec) == "function")
assert(type(__builtins.umask) == "function")
assert(__builtins == nil, "__builtins should not be in _G")
end
@@ -117,9 +114,9 @@ end
-- === exec ===
-- exec with no command returns error
-- exec with no command returns error (tested via backtick)
do
local r = __builtins.exec("exec")
local r = `exec`
assert(r.code == 1)
assert(r.stderr:find("command required"),
"expected 'command required', got: " .. r.stderr)
@@ -127,7 +124,7 @@ end
-- exec nonexistent command returns error
do
local r = __builtins.exec("exec", "nonexistent_cmd_xyz_999")
local r = `exec nonexistent_cmd_xyz_999`
assert(r.code == 1)
assert(r.stderr:find("exec:"),
"expected exec error, got: " .. r.stderr)
@@ -140,21 +137,4 @@ do
end
-- === builtins are overridable ===
do
local old_cd = __builtins.cd
local called = false
__builtins.cd = function(...)
called = true
return old_cd(...)
end
local before = `pwd`.stdout:gsub("\n$", "")
`cd /tmp`
assert(called, "custom cd was not called")
__builtins.cd = old_cd
`cd ${before}`
end
print "OK"