Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lind-sharedlib-poc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Per-example build outputs and generated guest modules.
build/
**/target/
*.wasm
*.cwasm
75 changes: 75 additions & 0 deletions lind-sharedlib-poc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Sandboxed shared libraries — PoC

Take a library, compile it to WebAssembly, run it as a guest inside the
lind/wasmtime sandbox, and expose it to the outside world as an ordinary native
`.so`. An **unmodified** native application links the `.so` and calls its
functions normally — unaware that the real work happens inside a wasm sandbox.
A **binary drop-in** for an unmodified app, not a recompile against a wrapper API.

## How this folder is organized

The reusable runtime lives in `src/` (the `lind-boot` refactor: `--call`,
`init_sandboxed_lib`, `SandboxedLib`). **This** tree holds only the per-iteration
material — one self-contained folder per example, each adding exactly one new
marshalling capability on top of the previous:

| Example | Adds |
| --- | --- |
| [`examples/01-scalars`](examples/01-scalars) | plumbing only — scalar `int(int,int)`, no marshalling |
| `examples/02-buffers` *(next)* | caller-allocated in/out buffer |
| `examples/03-strings` | NUL-terminated string copy across the boundary |
| `examples/04-structs` | struct copy + ILP32/LP64 layout |
| `examples/05-callbacks` | guest trampoline re-entering the host |
| `examples/06-concurrency` | drop the global lock |

### Anatomy of an example

Every example has the same shape, so the next one is a copy of the last:

```
examples/NN-name/
guest.c the library — compiled to guest.cwasm, runs in the sandbox
demo.c an unmodified native program that links the .so
functions.txt stub manifest: one exported signature per line
stub/ the cdylib crate -> libNAME.so (native symbols -> guest calls)
Makefile `include ../../common.mk`; sets LIB + a `check` target
```

Shared build logic is in [`common.mk`](common.mk); stub generation is in
[`tools/gen_stubs.sh`](tools/gen_stubs.sh).

## Working in an example

```bash
cd examples/01-scalars
make # build everything — runs nothing (build and run can be on different machines)
make run # run the demo against the wasm-sandboxed lib, via lind-wasm
make run-native # baseline: real native lib, no sandbox
make compare # run native then sandboxed, back to back
make gen # functions.txt -> stub/src/lib.rs (committed; regenerate on change)
make check # quick in-host smoke test (lind_run --call), no .so packaging
```

`make` only builds; running is explicit (`make run`) — running needs the full
Linux lind runtime, which the plain build does not.

Prerequisite: the toolchain built once from the repo root (`make build`).

### `make check` — the in-host debugging trick

Before packaging as a `.so`, you can exercise a guest export directly with the
`--call` flag, which runs a named export instead of `_start`:

```bash
./scripts/lind_run --call add examples/01-scalars/guest.cwasm 2 3 # -> [I32(5)]
```

This is the fastest way to confirm the guest side works in isolation from the
native linking / stub layer.

## Adding the next iteration

1. `cp -r examples/01-scalars examples/02-buffers`
2. Edit `guest.c`, `demo.c`, `functions.txt`; set a new `LIB` in the `Makefile`.
3. `make gen && make && make run` — for the scalar-only generator this is enough;
a pointer-using function is where `gen_stubs.sh` grows to emit marshalling glue.
130 changes: 130 additions & 0 deletions lind-sharedlib-poc/common.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Shared build engine for the sandboxed-library examples.
#
# An example Makefile sets `LIB` and `EXAMPLE_DIR`, then `include`s this file:
#
# LIB := add_sub
# EXAMPLE_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
# include ../../common.mk
#
# Every example has the same shape on disk:
# guest.c the library source -> guest.cwasm (runs in the sandbox)
# demo.c unmodified native caller
# functions.txt stub manifest -> stub/src/lib.rs (via `make gen`)
# stub/ the cdylib crate -> lib$(LIB).so
#
# Targets: native (baseline) | lind (sandboxed) | gen | guest | host | clean | help.

COMMON_MK := $(abspath $(lastword $(MAKEFILE_LIST)))
POC_DIR := $(patsubst %/,%,$(dir $(COMMON_MK)))
REPO_ROOT := $(abspath $(POC_DIR)/..)

CC ?= cc
LIND_COMPILE := $(REPO_ROOT)/scripts/lind_compile
LIND_RUN := $(REPO_ROOT)/scripts/lind_run
GEN := $(POC_DIR)/tools/gen_stubs.sh

# Per-example sources (fixed filenames).
GUEST_SRC := $(EXAMPLE_DIR)/guest.c
DEMO_SRC := $(EXAMPLE_DIR)/demo.c
FUNCS := $(EXAMPLE_DIR)/functions.txt
STUB_DIR := $(EXAMPLE_DIR)/stub
STUB_LIB := $(STUB_DIR)/src/lib.rs
STUB_MANIFEST := $(STUB_DIR)/Cargo.toml
CDYLIB_DIR := $(STUB_DIR)/target/release
CDYLIB := $(CDYLIB_DIR)/lib$(LIB).so

# Guest module. lind_compile (full mode) writes .wasm + .cwasm next to the source
# AND copies the .cwasm into lindfs/, because lind can only locate modules inside
# lindfs/ (lind_run chroots into it). `--output-dir` puts each example's module in
# its own lindfs subdir so the per-example `guest.cwasm` names don't collide.
# NOTE: no inline comments on these value lines — Make would keep the whitespace
# before the `#` as part of the path.
GUEST_WASM := $(EXAMPLE_DIR)/guest.wasm
# .cwasm written next to the source:
GUEST_CWASM_SRC := $(EXAMPLE_DIR)/guest.cwasm
LINDFS_DIR := $(REPO_ROOT)/lindfs
# per-example subdir under lindfs/ (avoids guest.cwasm name collisions):
LINDFS_SUBDIR := sharedlib-poc/$(LIB)
# host path to the lindfs copy (what LIND_MODULE / the .so reads):
GUEST_MODULE := $(LINDFS_DIR)/$(LINDFS_SUBDIR)/guest.cwasm
# path as lind_run sees it after chrooting into lindfs/:
GUEST_LINDPATH := $(LINDFS_SUBDIR)/guest.cwasm

# Build outputs.
BUILD := $(EXAMPLE_DIR)/build
NATIVE_DIR := $(BUILD)/native
LIND_DIR := $(BUILD)/lind

.DEFAULT_GOAL := build
.PHONY: build run run-native compare gen guest host clean help FORCE

# `make` (default) only BUILDS — it produces every artifact but runs nothing.
# This matters because build and run can happen on different machines: the guest
# module + cdylib build anywhere, but *running* needs the full Linux lind runtime.
# Use `make run` / `make run-native` to execute.
build: $(NATIVE_DIR)/demo $(LIND_DIR)/demo $(GUEST_MODULE)
@echo "built native + sandboxed demos — run with 'make run' (or 'make run-native')"

# Regenerate the extern "C" stubs from functions.txt.
gen:
$(GEN) $(FUNCS) > $(STUB_LIB)
@echo "generated $(STUB_LIB)"

# --------------------------------------------------------------------------
# Native baseline: compile guest.c as an ordinary shared library and link the
# unmodified demo against it. The control case — no sandbox.
# --------------------------------------------------------------------------
run-native: $(NATIVE_DIR)/demo
@echo "=================== NATIVE (baseline) ==================="
@LD_LIBRARY_PATH=$(NATIVE_DIR) $(NATIVE_DIR)/demo

# -w silences the wasm-only `export_name` attribute warning on native targets.
$(NATIVE_DIR)/lib$(LIB).so: $(GUEST_SRC) | $(NATIVE_DIR)
$(CC) -shared -fPIC -w -o $@ $(GUEST_SRC)

$(NATIVE_DIR)/demo: $(DEMO_SRC) $(NATIVE_DIR)/lib$(LIB).so | $(NATIVE_DIR)
$(CC) $(DEMO_SRC) -L$(NATIVE_DIR) -l$(LIB) -o $@

# --------------------------------------------------------------------------
# Sandboxed path: compile guest.c to wasm, build the wasm-backed cdylib, and link
# the SAME demo against it. The guest functions run inside the lind/wasmtime cage.
# --------------------------------------------------------------------------
run: $(LIND_DIR)/demo $(GUEST_MODULE)
@echo "================ LIND (wasm-sandboxed) ================="
@LIND_MODULE=$(GUEST_MODULE) LD_LIBRARY_PATH=$(CDYLIB_DIR) $(LIND_DIR)/demo

# Run both, back to back, for comparison.
compare: run-native run

# Compile the guest and land it in its lindfs subdir. lind_compile writes the
# .cwasm next to the source and copies it into lindfs/$(LINDFS_SUBDIR)/.
guest: $(GUEST_MODULE)
$(GUEST_MODULE): $(GUEST_SRC)
$(LIND_COMPILE) --output-dir $(LINDFS_SUBDIR) $(GUEST_SRC)

# Host shim .so (embeds wasmtime + lind). FORCE-built so cargo — not make —
# decides what needs rebuilding across the whole lind-boot dependency graph.
host: $(CDYLIB)
$(CDYLIB): FORCE
cargo build --release --manifest-path $(STUB_MANIFEST)

$(LIND_DIR)/demo: $(DEMO_SRC) $(CDYLIB) | $(LIND_DIR)
$(CC) $(DEMO_SRC) -L$(CDYLIB_DIR) -l$(LIB) -o $@

$(NATIVE_DIR) $(LIND_DIR):
mkdir -p $@

clean:
rm -rf $(BUILD)
rm -f $(GUEST_WASM) $(GUEST_CWASM_SRC)
rm -rf $(LINDFS_DIR)/$(LINDFS_SUBDIR)

help:
@echo "make - build everything (runs nothing)"
@echo "make run - run the demo against the wasm-sandboxed lib$(LIB).so"
@echo "make run-native - run the demo against a real native lib$(LIB).so (baseline)"
@echo "make compare - run native then sandboxed, back to back"
@echo "make gen - regenerate stub/src/lib.rs from functions.txt"
@echo "make guest - compile guest.c -> guest.cwasm only"
@echo "make host - build the cdylib (lib$(LIB).so) only"
@echo "make clean - remove build artifacts"
25 changes: 25 additions & 0 deletions lind-sharedlib-poc/examples/01-scalars/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Example 01 — scalars.
#
# A trivial library (int add(int,int), int subtract(int,int)) exposed as a native
# .so whose symbols run inside the lind/wasmtime sandbox. Pure scalar i32 in/out,
# so nothing crosses the guest memory boundary — this proves the runtime +
# packaging plumbing in isolation, before any marshalling.
#
# make build everything (runs nothing)
# make run run the demo against the wasm-sandboxed libadd_sub.so
# make run-native run the demo against a real native libadd_sub.so (baseline)
# make compare run native then sandboxed, back to back
# make gen regenerate stub/src/lib.rs from functions.txt
# make check in-host smoke test via `lind_run --call` (no .so packaging)

LIB := add_sub
EXAMPLE_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
include ../../common.mk

# In-host smoke test of the Stage-1 mechanic. Function names + args are
# example-specific, so this lives here rather than in the shared engine.
# lind_run chroots into lindfs/, so the module is the lindfs-relative path.
.PHONY: check
check: $(GUEST_MODULE)
$(LIND_RUN) --call add $(GUEST_LINDPATH) 2 3
$(LIND_RUN) --call subtract $(GUEST_LINDPATH) 10 4
47 changes: 47 additions & 0 deletions lind-sharedlib-poc/examples/01-scalars/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Example 01 — scalars

The smallest end-to-end slice: a native `libadd_sub.so` whose `add`/`subtract`
symbols run their real implementations inside the lind/wasmtime sandbox, called
by an unmodified native `demo.c`.

Both functions are pure scalar `int(int,int)`, so **nothing crosses the guest
memory boundary** — a wasm `i32` *is* a native `int`. This isolates the runtime
+ packaging plumbing from marshalling, which later examples add.

## Pieces

| File | Role |
| --- | --- |
| `guest.c` | the library; exported to wasm, compiled to `guest.cwasm` (runs in the cage) |
| `demo.c` | unmodified native caller; links `-ladd_sub` |
| `functions.txt` | stub manifest (`add`/`subtract`, both scalar) |
| `stub/` | the cdylib crate → `libadd_sub.so`: native `add`/`subtract` symbols that forward into the guest |

The stub crate depends on `lind-boot` (`../../../../src/lind-boot`) and, through
it, the whole lind/wasmtime/rawposix/3i stack.

## Run

```bash
make gen # functions.txt -> stub/src/lib.rs
make # build everything (runs nothing)
make run # run the wasm-sandboxed demo
make run-native # run the baseline (no sandbox)
make check # in-host smoke test via lind_run --call
```

Expected output (identical for `run` and `run-native`):

```
add(2, 3) = 5
subtract(10, 4) = 6
```

## Intentional simplifications

- **Scalars only** — no pointers, buffers, structs, callbacks, or threads.
- **No `__wasm_call_ctors`** — the guest has no `main`/`_start`; `add`/`subtract`
touch no libc/global state, so skipping constructors is fine here.
- **Global lock** — a wasmtime `Store` is not `Sync`, so the resident instance
lives behind a `Mutex` and calls are serialized.
- **No chroot** — a loaded library must not chroot its host process.
13 changes: 13 additions & 0 deletions lind-sharedlib-poc/examples/01-scalars/demo.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* A plain native program. It links against libadd_sub.so and calls add/subtract
* with no idea that those run inside the lind/wasmtime sandbox. This is the
* "unmodified drop-in" consumer for the Stage-2 PoC. */
#include <stdio.h>

int add(int a, int b);
int subtract(int a, int b);

int main(void) {
printf("add(2, 3) = %d\n", add(2, 3));
printf("subtract(10, 4) = %d\n", subtract(10, 4));
return 0;
}
9 changes: 9 additions & 0 deletions lind-sharedlib-poc/examples/01-scalars/functions.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Stub manifest for this example. One exported function per line:
#
# <name> <ret-type> <arg-type>...
#
# Scalar stage: every type is i32, so only the arg count shapes each stub.
# `make gen` turns this into stub/src/lib.rs.

add i32 i32 i32
subtract i32 i32 i32
19 changes: 19 additions & 0 deletions lind-sharedlib-poc/examples/01-scalars/guest.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Stage-1 PoC: a trivial "library" exposed to the lind host as a long-lived
// reactor. It exports two pure-scalar functions (int(int,int)) that the host
// can call by name via `lind-boot --call <name>`.
//
// `export_name` makes each function appear as a Wasm export under exactly that
// name, so the host's `instance.get_func("add")` lookup succeeds. There is no
// `main`/`_start`: `--call` invokes an export directly, so the module needs no
// entry point. Build it with the default (dynamic) `lind_compile` mode — the
// static `-s` mode would require an entry point.

__attribute__((export_name("add")))
int add(int a, int b) {
return a + b;
}

__attribute__((export_name("subtract")))
int subtract(int a, int b) {
return a - b;
}
16 changes: 16 additions & 0 deletions lind-sharedlib-poc/examples/01-scalars/stub/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "lind-libadd-sub"
version = "0.1.0"
edition = "2024"

# Build a native shared object. `name = "add_sub"` makes the output `libadd_sub.so`,
# so a native program can link it with `-ladd_sub`.
[lib]
name = "add_sub"
crate-type = ["cdylib"]

[dependencies]
# Reuses lind-boot's sandboxed-lib API (init_sandboxed_lib / SandboxedLib) and, through
# it, the whole lind/wasmtime/rawposix/3i stack. Default features match how lind-boot
# itself is normally built (fdtables-dashmaparray, signals on, wasm-EH setjmp).
lind-boot = { path = "../../../../src/lind-boot" }
50 changes: 50 additions & 0 deletions lind-sharedlib-poc/examples/01-scalars/stub/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// @generated by tools/gen_stubs.sh from functions.txt — do not edit by hand.
// Regenerate with `make gen`.
//
// Native shared library whose exported symbols run their real implementations
// inside the lind/wasmtime sandbox. A native app links this like any other .so
// and calls the symbols with no knowledge that the work happens in a wasm guest.
// On the first call the lind runtime is brought up and the guest module is
// instantiated as a long-lived sandboxed library; later calls reuse it.
//
// Scope (scalar stage): i32 in/out only, a single global instance, and a global
// lock serializing every call (a wasmtime Store is not Sync).

use std::sync::{Mutex, OnceLock};

use lind_boot::{CliOptions, SandboxedLib, init_sandboxed_lib};

static LIB: OnceLock<Mutex<SandboxedLib>> = OnceLock::new();

/// Path to the precompiled guest module. Overridable via `LIND_MODULE`; defaults
/// to `guest.cwasm` in the process's working directory.
fn module_path() -> String {
std::env::var("LIND_MODULE").unwrap_or_else(|_| "guest.cwasm".to_string())
}

fn lib() -> &'static Mutex<SandboxedLib> {
LIB.get_or_init(|| {
let cli = CliOptions::for_sandboxed_lib(module_path());
let sandboxed_lib = init_sandboxed_lib(cli)
.unwrap_or_else(|e| panic!("lind sandboxed-lib init failed: {e:?}"));
Mutex::new(sandboxed_lib)
})
}

fn call(name: &str, args: &[i32]) -> i32 {
lib()
.lock()
.unwrap()
.call_scalar(name, args)
.unwrap_or_else(|e| panic!("lind call `{name}` failed: {e:?}"))
}

#[unsafe(no_mangle)]
pub extern "C" fn add(a0: i32, a1: i32) -> i32 {
call("add", &[a0, a1])
}

#[unsafe(no_mangle)]
pub extern "C" fn subtract(a0: i32, a1: i32) -> i32 {
call("subtract", &[a0, a1])
}
Loading