Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
80 changes: 80 additions & 0 deletions lind-sharedlib-poc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 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.
Same idea as RLBox, but a stricter constraint: a **binary drop-in** for an
unmodified app, not a recompile against a wrapper API.

The full design write-up is in
[`docs/internal/sandboxed-library.md`](../docs/internal/sandboxed-library.md).

## 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-strings`](examples/02-strings) | copy-in of a `const char*` (first marshalled argument) |
| [`examples/03-buffers`](examples/03-buffers) | caller-allocated output buffers / copy-out (all four length contracts) |
| [`examples/04-inout`](examples/04-inout) | in/out buffers — the guest reads and writes the caller's buffer (in-place) |
| [`examples/05-structs`](examples/05-structs) | `const struct*` input — field-by-field marshalling across ILP32/LP64 |
| `examples/06-callbacks` *(next)* | guest trampoline re-entering the host |
| `examples/07-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.
75 changes: 75 additions & 0 deletions lind-sharedlib-poc/README.md.1
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.
135 changes: 135 additions & 0 deletions lind-sharedlib-poc/common.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# 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.
# --------------------------------------------------------------------------
# PRELOAD (optional, set by an example): host paths of guest libraries to preload
# into the sandbox, e.g. libc/libm for a guest that calls them. Same `name=path`
# syntax as lind-boot's --preload, comma-separated. The .so reads it via LIND_PRELOAD.
PRELOAD ?=

run: $(LIND_DIR)/demo $(GUEST_MODULE)
@echo "================ LIND (wasm-sandboxed) ================="
@LIND_MODULE=$(GUEST_MODULE) LIND_PRELOAD="$(PRELOAD)" 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"
Loading