Embedding Lua in a Shell Script

2021-08-01

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:

FileSize
lua5.1191k
lua5.1 base64258k
lua5.1 zip86k
lua5.1 base64+zip116k

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