The goal of this small project is to create a shell script that can run Lua code, either passed to it, or embedded within it.
Embedding the Lua Executable
I'm using Lua 5.1 for this. The executable won't be particularly portable since lua5.1
is dynamically linked:
rpdillon@incipio:~$ ldd $(which lua5.1)
linux-vdso.so.1 (0x00007ffc658f8000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f5941858000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f5941851000)
libreadline.so.8 => /lib/x86_64-linux-gnu/libreadline.so.8 (0x00007f59417fd000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5941611000)
/lib64/ld-linux-x86-64.so.2 (0x00007f59419f3000)
libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007f59415e2000)
This means it's also pretty small! lua5.1
has a size of 191k on my Pop!_OS machine (20.04-based). Looking at lua
installed in an Alpine container, it's only 21k! These are perfectly manageable sizes to embed it in the script. My approach is to use base64
, which can increase the size , but if we zip the executable first, we'll gain all of that back and then some. Focusing on the Pop!_OS version of lua5.1
, here's how the sizes look:
File | Size |
lua5.1 | 191k |
lua5.1 base64 | 258k |
lua5.1 zip | 86k |
lua5.1 base64+zip | 116k |
Using base64 with a zipped payload gives a reasonable size to embed in a script using a heredoc:
LUAZIP=$(cat <<EOF
<base64 payload here>
EOF
)
Extracting the Interpreter
The most straightforward approach is to extract the interpreter to disk, unzip it, and then invoke it with the desired script. The downside is that this litters the disk with files.
I wasn't able to find an approach that would allow bypassing the filesystem to operate directly in RAM. The idea here would be to pass LUAZIP
to the base64 -d
, and then pass that stream to unzip
, and finally take the resulting bytestream, treat it as executable, and run it. Conceptually, this is all fine. In practice, I wasn't able to coerce unzip
to work on the output of base64 -d
directly, and even if I could, I don't see any way to execute a bytestream without the filesystem coming into play. Areas for future work!
So the next best thing is leveraging tmpfs
. This keeps the filesystem in play, but backs it with RAM. Unfortunately, mounting a tmpfs
filesystem needs superuser privileges, which precludes mounting a dedicated tmpfs
volume on script execution. But there is a tmpfs
filesystem mounted in most every Linux distribution I've worked with: /dev/shm
. What if we use that?
WD=/dev/shm
if [ ! -w "${WD}" ]; then
echo "Unable to write to ${WD}, aborting."
exit 255
fi
echo $LUAZIP | base64 -d > $WD/lua.zip
unzip -qq -d $WD $WD/lua.zip
chmod +x $WD/lua5.1
$WD/lua5.1
rm $WD/lua.zip
rm $WD/lua5.1