Skip to content

Commit 86cb5ae

Browse files
committed
Merge fix/issue-11-shared-lib-segfault: fix shared lib segfault on Linux x86_64
Fixes #11. Root cause: Zig 0.15's GPA crashes in Debug-mode PIC on Linux x86_64. Fix: c_allocator + default ReleaseSafe for libraries. Adds 64-test FFI test suite (dlopen-based), CI integration.
2 parents d83c0ff + e2696a5 commit 86cb5ae

6 files changed

Lines changed: 628 additions & 45 deletions

File tree

.claude/CLAUDE.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,13 @@ When in doubt, **continue**.
8989
(Required when modifying interpreter/opcodes)
9090
4. **Real-world compat**: `bash test/realworld/run_compat.sh` — PASS=50, FAIL=0, CRASH=0
9191
(Required when modifying vm/wasi/JIT)
92-
5. **Benchmarks**: Required for optimization/JIT tasks.
92+
5. **FFI tests**: `bash test/c_api/run_ffi_test.sh --build` — 0 failed
93+
(Required when modifying c_api.zig, build.zig lib targets, or include/zwasm.h)
94+
6. **Benchmarks**: Required for optimization/JIT tasks.
9395
- Quick check: `bash bench/run_bench.sh --quick`
9496
- **Record**: `bash bench/record.sh --id=ID --reason="REASON"` (appends to history.yaml)
95-
6. **Size guard**: Binary ≤ 1.5MB (stripped), memory ≤ 4.5MB RSS
96-
7. **decisions.md / checklist.md / spec-support.md / memo.md**: Update as needed
97+
7. **Size guard**: Binary ≤ 1.5MB (stripped), memory ≤ 4.5MB RSS
98+
8. **decisions.md / checklist.md / spec-support.md / memo.md**: Update as needed
9799

98100
### Merge Gate Checklist
99101

@@ -102,6 +104,7 @@ When in doubt, **continue**.
102104
- `python3 test/spec/run_spec.py --build --summary` — fail=0, skip=0
103105
- `bash test/e2e/run_e2e.sh --convert --summary` — fail=0, leak=0
104106
- `bash test/realworld/run_compat.sh` — PASS=50, FAIL=0, CRASH=0
107+
- `bash test/c_api/run_ffi_test.sh --build` — 0 failed
105108
- Benchmarks pass (no regression)
106109
Fix root cause before merging if Ubuntu reveals new failures.
107110

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ jobs:
5050
- name: Run C API tests
5151
run: zig build c-test
5252

53+
- name: Build shared library
54+
if: runner.os != 'Windows'
55+
run: zig build shared-lib
56+
57+
- name: Run FFI tests (shared library)
58+
if: runner.os != 'Windows'
59+
run: bash test/c_api/run_ffi_test.sh
60+
5361
- name: Setup Rust
5462
run: |
5563
source .github/tool-versions

build.zig

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,15 @@ pub fn build(b: *std.Build) void {
143143
}
144144

145145
// Shared library (libzwasm.dylib / libzwasm.so)
146+
// Default to ReleaseSafe: Zig 0.15's Debug-mode shared libraries
147+
// crash on Linux x86_64 due to GPA/PIC codegen issues (see #11).
148+
// Users embedding zwasm want optimized code anyway.
149+
const lib_optimize = b.option(bool, "lib-debug", "Build libraries in Debug mode (default: false)") orelse false;
146150
const lib_shared_mod = b.createModule(.{
147151
.root_source_file = b.path("src/c_api.zig"),
148152
.target = target,
149-
.optimize = optimize,
153+
.optimize = if (lib_optimize) optimize else if (optimize == .Debug) .ReleaseSafe else optimize,
154+
.link_libc = true,
150155
});
151156
lib_shared_mod.addOptions("build_options", options);
152157
const lib_shared = b.addLibrary(.{
@@ -160,7 +165,8 @@ pub fn build(b: *std.Build) void {
160165
const lib_static_mod = b.createModule(.{
161166
.root_source_file = b.path("src/c_api.zig"),
162167
.target = target,
163-
.optimize = optimize,
168+
.optimize = if (lib_optimize) optimize else if (optimize == .Debug) .ReleaseSafe else optimize,
169+
.link_libc = true,
164170
});
165171
lib_static_mod.addOptions("build_options", options);
166172
const lib_static = b.addLibrary(.{
@@ -170,10 +176,18 @@ pub fn build(b: *std.Build) void {
170176
});
171177
lib_static.installHeader(b.path("include/zwasm.h"), "zwasm.h");
172178

179+
// Separate steps to avoid Zig 0.15.2 build graph shuffle bug
180+
// when two same-named artifacts are in the same step.
181+
const shared_lib_step = b.step("shared-lib", "Build shared library (libzwasm.so/.dylib)");
182+
shared_lib_step.dependOn(&b.addInstallArtifact(lib_shared, .{}).step);
183+
184+
const static_lib_step = b.step("static-lib", "Build static library (libzwasm.a)");
185+
static_lib_step.dependOn(&b.addInstallArtifact(lib_static, .{}).step);
186+
173187
// "lib" step builds both shared and static libraries
174188
const lib_step = b.step("lib", "Build shared and static libraries");
175-
lib_step.dependOn(&b.addInstallArtifact(lib_shared, .{}).step);
176-
lib_step.dependOn(&b.addInstallArtifact(lib_static, .{}).step);
189+
lib_step.dependOn(shared_lib_step);
190+
lib_step.dependOn(static_lib_step);
177191

178192
// C API test executables (link against static library)
179193
const c_tests = [_]struct { name: []const u8, src: []const u8 }{

src/c_api.zig

Lines changed: 18 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
//! layout. Error messages are stored in a thread-local buffer accessible
99
//! via `zwasm_last_error_message()`.
1010
//!
11-
//! Allocator strategy: Each CApiModule owns a GeneralPurposeAllocator,
12-
//! heap-allocated via page_allocator so its address is stable. The GPA
13-
//! provides the allocator for WasmModule and all its internal state.
11+
//! Allocator strategy: The C API uses libc malloc (c_allocator) as the
12+
//! default backing allocator for WasmModule and all its internal state.
13+
//! Custom allocators can be injected via zwasm_config_t.
1414

1515
const std = @import("std");
1616
const types = @import("types.zig");
@@ -97,20 +97,22 @@ const CApiConfig = struct {
9797
pub const zwasm_config_t = CApiConfig;
9898

9999
// ============================================================
100-
// Internal wrapper — GPA + WasmModule co-located
100+
// Internal wrapper — allocator + WasmModule co-located
101101
// ============================================================
102102

103-
const Gpa = std.heap.GeneralPurposeAllocator(.{});
103+
// Zig 0.15's GeneralPurposeAllocator crashes in Debug-mode shared
104+
// libraries on Linux x86_64 (PIC codegen issue, see GitHub #11).
105+
// The C API uses libc malloc (c_allocator) as the default backing
106+
// allocator, which is correct for a library loaded via dlopen/ctypes.
107+
// GPA is only used when running Zig tests (leak detection).
108+
const default_allocator = std.heap.c_allocator;
104109

105-
/// Internal wrapper owning both the GPA and WasmModule.
110+
/// Internal wrapper owning a WasmModule.
106111
/// Heap-allocated via page_allocator for address stability.
107112
///
108-
/// When a custom allocator is injected via zwasm_config_t, gpa is unused
109-
/// and the custom allocator handles all allocations. The `owns_gpa` flag
110-
/// tracks whether we need to deinit gpa.
113+
/// When a custom allocator is injected via zwasm_config_t, that
114+
/// allocator is used instead of the default.
111115
const CApiModule = struct {
112-
gpa: Gpa,
113-
owns_gpa: bool,
114116
module: *WasmModule,
115117

116118
fn create(wasm_bytes: []const u8, wasi: bool) !*CApiModule {
@@ -121,15 +123,7 @@ const CApiModule = struct {
121123
const self = try std.heap.page_allocator.create(CApiModule);
122124
errdefer std.heap.page_allocator.destroy(self);
123125

124-
const allocator = if (custom_alloc) |ca| blk: {
125-
self.gpa = .{};
126-
self.owns_gpa = false;
127-
break :blk ca;
128-
} else blk: {
129-
self.gpa = .{};
130-
self.owns_gpa = true;
131-
break :blk self.gpa.allocator();
132-
};
126+
const allocator = custom_alloc orelse default_allocator;
133127

134128
self.module = if (wasi)
135129
try WasmModule.loadWasi(allocator, wasm_bytes)
@@ -146,15 +140,7 @@ const CApiModule = struct {
146140
const self = try std.heap.page_allocator.create(CApiModule);
147141
errdefer std.heap.page_allocator.destroy(self);
148142

149-
const allocator = if (custom_alloc) |ca| blk: {
150-
self.gpa = .{};
151-
self.owns_gpa = false;
152-
break :blk ca;
153-
} else blk: {
154-
self.gpa = .{};
155-
self.owns_gpa = true;
156-
break :blk self.gpa.allocator();
157-
};
143+
const allocator = custom_alloc orelse default_allocator;
158144

159145
self.module = try WasmModule.loadWasiWithOptions(allocator, wasm_bytes, opts);
160146
return self;
@@ -163,16 +149,12 @@ const CApiModule = struct {
163149
fn createWithImports(wasm_bytes: []const u8, imports: []const types.ImportEntry) !*CApiModule {
164150
const self = try std.heap.page_allocator.create(CApiModule);
165151
errdefer std.heap.page_allocator.destroy(self);
166-
self.gpa = .{};
167-
self.owns_gpa = true;
168-
const allocator = self.gpa.allocator();
169-
self.module = try WasmModule.loadWithImports(allocator, wasm_bytes, imports);
152+
self.module = try WasmModule.loadWithImports(default_allocator, wasm_bytes, imports);
170153
return self;
171154
}
172155

173156
fn destroy(self: *CApiModule) void {
174157
self.module.deinit();
175-
if (self.owns_gpa) _ = self.gpa.deinit();
176158
std.heap.page_allocator.destroy(self);
177159
}
178160
};
@@ -392,9 +374,7 @@ export fn zwasm_module_delete(module: *zwasm_module_t) void {
392374
/// Returns true if valid, false if invalid or malformed.
393375
export fn zwasm_module_validate(wasm_ptr: [*]const u8, len: usize) bool {
394376
clearError();
395-
var gpa = Gpa{};
396-
defer _ = gpa.deinit();
397-
const allocator = gpa.allocator();
377+
const allocator = default_allocator;
398378
const validate = types.runtime.validateModule;
399379
var module = types.runtime.Module.init(allocator, wasm_ptr[0..len]);
400380
defer module.deinit();
@@ -906,7 +886,7 @@ const MEMORY_WASM = "\x00\x61\x73\x6d\x01\x00\x00\x00" ++
906886
"\x01\x04\x01\x60\x00\x00" ++ // type: () -> ()
907887
"\x03\x02\x01\x00" ++ // func section
908888
"\x05\x03\x01\x00\x01" ++ // memory: min=0, max=1
909-
"\x07\x0d\x02\x01\x6d\x02\x00" ++ // export "m" = memory 0
889+
"\x07\x09\x02\x01\x6d\x02\x00" ++ // export "m" = memory 0
910890
"\x01\x66\x00\x00" ++ // export "f" = func 0
911891
"\x0a\x0b\x01\x09\x00\x41\x00\x41\x2a\x36\x02\x00\x0b"; // code: i32.const 0, i32.const 42, i32.store, end
912892

test/c_api/run_ffi_test.sh

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env bash
2+
# run_ffi_test.sh — Build libzwasm shared lib and run FFI test suite
3+
#
4+
# Usage:
5+
# bash test/c_api/run_ffi_test.sh [--build]
6+
#
7+
# Options:
8+
# --build Rebuild the shared library before testing (default: skip if exists)
9+
10+
set -euo pipefail
11+
cd "$(dirname "$0")/../.."
12+
13+
BUILD=false
14+
for arg in "$@"; do
15+
case "$arg" in
16+
--build) BUILD=true ;;
17+
esac
18+
done
19+
20+
# Detect platform
21+
if [[ "$(uname)" == "Darwin" ]]; then
22+
LIB="zig-out/lib/libzwasm.dylib"
23+
else
24+
LIB="zig-out/lib/libzwasm.so"
25+
fi
26+
27+
# Build shared library if requested or missing
28+
if $BUILD || [ ! -f "$LIB" ]; then
29+
echo "Building shared library..."
30+
zig build shared-lib
31+
fi
32+
33+
# Compile test binary
34+
echo "Compiling FFI test..."
35+
gcc -o /tmp/zwasm_ffi_test test/c_api/test_ffi.c -ldl -O0 -g
36+
37+
# Run
38+
echo ""
39+
/tmp/zwasm_ffi_test "$LIB"
40+
EXIT=$?
41+
42+
# Cleanup
43+
rm -f /tmp/zwasm_ffi_test
44+
45+
exit $EXIT

0 commit comments

Comments
 (0)