Run LLVM 18 Bitcode Safely with lli-18

lli-18 runs LLVM bitcode directly, which is exactly what you want when a .bc file lands in your lap with zero explanation. You will finish with a repeatable way to run it, pass arguments, pick a non-default entry function, and read the exit status correctly. The examples use the installed Ubuntu LLVM 18.1.3 build from package llvm-18-runtime, version 1:18.1.3-1ubuntu1.

Allow about ten minutes. You need a shell and a .bc file; the small test program below also needs llvm-as-18 to assemble LLVM IR.

Warning: lli-18 executes code. Do not use it as a way to peek at bitcode from an untrusted source: run untrusted input in a proper sandbox, well away from secrets or writable production data.

1. Check the installed tool

Before anything else, check which lli-18 you are actually running and what version it reports:

$ command -v lli-18
/usr/bin/lli-18
$ lli-18 --version
Ubuntu LLVM version 18.1.3
  Optimized build.

The manpage describes lli as an LLVM interpreter and dynamic compiler. On this installation the command is named lli-18, and it takes an input bitcode file followed by any arguments for the program:

$ lli-18 [options] <input-bitcode> [program-arguments]

Checkpoint: if command -v comes back empty, stop there. Do not work around a missing executable by downloading some unrelated build into a system directory.

2. Make a harmless bitcode test file

Already have a bitcode file to test? Skip ahead. Otherwise build this tiny LLVM IR module in a scratch directory. It returns status 7 and prints nothing, which makes the result trivial to check:

$ mkdir -p /tmp/lli-18-check
$ cat > /tmp/lli-18-check/status.ll <<'EOF'
define i32 @main(i32 %argc, ptr %argv) {
  ret i32 7
}

define i32 @alternate(i32 %argc, ptr %argv) {
  ret i32 23
}
EOF
$ llvm-as-18 /tmp/lli-18-check/status.ll -o /tmp/lli-18-check/status.bc
$ file /tmp/lli-18-check/status.bc
/tmp/lli-18-check/status.bc: LLVM IR bitcode

That temporary directory is just a scratch pad, delete it whenever. If your shell reports llvm-as-18 as unavailable, use a bitcode file from your normal LLVM toolchain instead, or install the matching development tools through your system's usual package process. Do not bolt an assembler onto a deployment box merely to run one existing bitcode file.

Checkpoint: the assembler must exit clean and the .bc file must exist before you point lli-18 at it.

3. Run the default entry function

Run the bitcode by giving its path as the first non-option argument:

$ lli-18 /tmp/lli-18-check/status.bc
$ status=$?
$ printf 'lli-18 exit status: %s\n' "$status"
lli-18 exit status: 7

With no entry-function option, lli-18 looks for main. A clean launch does not mean a zero exit status: the command hands back whatever the program itself returned. Grab $? straight away, because the next command you run will overwrite it.

The file name has to refer to valid bitcode. A missing input, for example, produces an error and status 1 on this installation:

$ lli-18 /tmp/lli-18-check/missing.bc
lli-18: lli: /tmp/lli-18-check/missing.bc: error: Could not open input file: No such file or directory
$ printf '%s\n' "$?"
1

4. Pass arguments after the bitcode path

Arguments after the input file belong to the executed program, not to lli-18. Keep the bitcode path in front of them:

$ lli-18 /path/to/program.bc --config /path/to/example.conf --mode check

Quote anything that might contain spaces. The classic mistake is putting an application flag before the bitcode path: the command-line parser grabs it as an lli-18 option, and the program never sees it.

Need the running program to see a different argv[0]? Use --fake-argv0=/path/to/name. It only changes what the program is told; it does not rename the bitcode file or create an executable.

5. Select another entry function

The test module also defines alternate, so name it explicitly:

$ lli-18 --entry-function=alternate /tmp/lli-18-check/status.bc
$ printf 'lli-18 exit status: %s\n' "$?"
lli-18 exit status: 23

The default is always main. Whatever function you pick needs a compatible entry signature: do not aim --entry-function at some random helper just because it exists, since an incompatible one can fail during execution or produce undefined behaviour.

Checkpoint: if changing the entry function changes the exit status the way you expect, you have verified both option placement and entry-point selection.

6. Choose the execution mode deliberately

LLVM can run the module with a just-in-time compiler or with an interpreter. The installed help lists these controls:

For a deterministic interpreter comparison, run:

$ lli-18 --force-interpreter /tmp/lli-18-check/status.bc
$ printf 'interpreter status: %s\n' "$?"
interpreter status: 7

Day to day, leave these switches at their defaults unless you are testing a specific engine or measuring behaviour. Code-generator options are not automatically meaningful to the interpreter, and changing optimisation or JIT settings can shift performance without changing what the program is meant to do.

7. Handle linking and failure boundaries

lli-18 also exposes --extra-module, --extra-object, --extra-archive, and --dlopen for loading additional code. Reach for them only when the module's build and linking design actually requires it.

Warning: treat every extra object, archive, plugin or shared library you load as executable input, no different from the bitcode itself. --load loads an LLVM plugin, which is exactly as security-sensitive as it sounds: check every path first.

Warning: do not use sudo for normal execution. Elevated privileges just give a buggy or malicious module a bigger blast radius. If a test genuinely needs a privileged resource, build a narrowly scoped test environment first and get the privilege through your normal operational review process.

When a run fails, separate input errors from program errors:

$ test -r /path/to/program.bc && echo 'bitcode is readable'
$ lli-18 /path/to/program.bc
$ status=$?
$ printf 'exit status: %s\n' "$status"
$ lli-18 --help | less

A non-zero status can be the program's deliberate result, a loader failure, or an execution failure. The number alone does not tell you which, so read the diagnostic and reproduce with the smallest safe input. If the command runs inside a script with set -e, capture and classify expected non-zero application statuses before the shell exits out from under you.

Done means