From f886d5f1beb8ca361efd219d53d81aa91de410c2 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 25 May 2026 09:50:37 +0300 Subject: [PATCH] =?UTF-8?q?mem:=20reject=20call-conv=20mismatches=20at=20b?= =?UTF-8?q?are-fn=20=E2=86=92=20fn-ptr=20coercion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Passing a default-conv sx function to a `callconv(.c)` fn-pointer slot (e.g. pthread_create's start routine) used to silently mismatch ABIs: the C-side caller didn't supply __sx_ctx, so the sx-side body read its first user param as garbage. The bug surfaced as a SIGSEGV inside ANativeWindow_setBuffersGeometry on Android during chess bringup. Now the compiler rejects the coercion outright at the bare-fn name lookup site: error: call-convention mismatch: 'sx_handler' is declared with default sx convention but the target type expects callconv(.c) Also: `#foreign` declarations without an explicit `callconv` now default to `.c` instead of `.default`. Every external C symbol is by definition C-conv; the previous default silently typed `objc_msgSend` (et al.) as default-conv, so the check would fire on the consumer side when the user typed a fn-ptr as `callconv(.c)`. With the foreign-default fix, the existing typed-msgSend casts in `std/objc.sx` and `gpu/metal.sx` keep type-checking and the rule is "C-conv on both sides or neither." Caught by the new check (fixed in the same commit): - `ios_gl_proc` in `platform/uikit.sx` lacked callconv(.c) but was passed to `load_gl` whose `get_proc` slot expects it. - `ffi_apply_callback` / `ffi_apply_callback2` in `examples/ffi-06-callback.sx` had default-conv fn-ptr params but the C bodies (in the companion .c) are unambiguously C-conv. Regression test: `examples/131-callconv-mismatch-diagnostic.sx` locks in the diagnostic shape (sx-conv fn → callconv(.c) slot). 153/153 example tests pass. Chess green on macOS / iOS sim / Android. --- examples/131-callconv-mismatch-diagnostic.sx | 14 ++++++++++ examples/ffi-06-callback.sx | 4 +-- library/modules/platform/uikit.sx | 3 ++- src/ir/lower.zig | 26 ++++++++++++++++++- .../131-callconv-mismatch-diagnostic.exit | 1 + .../131-callconv-mismatch-diagnostic.txt | 1 + .../ffi-jni-call-03-methodid-sharing.ir | 2 -- tests/expected/ffi-jni-call-04-jint-return.ir | 2 -- .../expected/ffi-jni-call-05-jlong-return.ir | 2 -- .../ffi-jni-call-06-jdouble-return.ir | 2 -- .../ffi-jni-call-07-jboolean-return.ir | 2 -- .../ffi-jni-call-08-jobject-return.ir | 2 -- tests/expected/ffi-jni-call-09-static.ir | 2 -- tests/expected/ffi-jni-class-08-call.ir | 2 -- .../expected/ffi-jni-env-02-lexical-direct.ir | 2 -- .../ffi-objc-call-03-selector-sharing.ir | 2 -- .../expected/ffi-objc-call-06-sret-return.ir | 2 -- 17 files changed, 45 insertions(+), 26 deletions(-) create mode 100644 examples/131-callconv-mismatch-diagnostic.sx create mode 100644 tests/expected/131-callconv-mismatch-diagnostic.exit create mode 100644 tests/expected/131-callconv-mismatch-diagnostic.txt diff --git a/examples/131-callconv-mismatch-diagnostic.sx b/examples/131-callconv-mismatch-diagnostic.sx new file mode 100644 index 0000000..f17685e --- /dev/null +++ b/examples/131-callconv-mismatch-diagnostic.sx @@ -0,0 +1,14 @@ +// Passing a default-conv sx function as a callconv(.c) fn-pointer +// silently mismatches ABIs — historically that meant the C-side caller +// supplied no `__sx_ctx` slot 0 and the sx-side body read garbage. +// The compiler now rejects the coercion outright with a "call-convention +// mismatch" diagnostic. + +#import "modules/std.sx"; + +sx_handler :: (arg: *void) -> *void { return arg; } + +main :: () -> s32 { + fp : (*void) -> *void callconv(.c) = sx_handler; + return 0; +} diff --git a/examples/ffi-06-callback.sx b/examples/ffi-06-callback.sx index 9e19e6b..3a7aa4e 100644 --- a/examples/ffi-06-callback.sx +++ b/examples/ffi-06-callback.sx @@ -16,8 +16,8 @@ #source "ffi-06-callback.c"; }; -ffi_apply_callback :: (cb: (s32) -> s32, value: s32) -> s32 #foreign; -ffi_apply_callback2 :: (cb: (*void, s32) -> s32, ctx: *void, v: s32) -> s32 #foreign; +ffi_apply_callback :: (cb: (s32) -> s32 callconv(.c), value: s32) -> s32 #foreign; +ffi_apply_callback2 :: (cb: (*void, s32) -> s32 callconv(.c), ctx: *void, v: s32) -> s32 #foreign; g_callback_hits : s32 = 0; g_callback_sum : s32 = 0; diff --git a/library/modules/platform/uikit.sx b/library/modules/platform/uikit.sx index 70d7497..6afe3d6 100644 --- a/library/modules/platform/uikit.sx +++ b/library/modules/platform/uikit.sx @@ -237,7 +237,8 @@ impl Platform for UIKitPlatform { } // dlsym(RTLD_DEFAULT, name) — Apple platforms. RTLD_DEFAULT is (void*)-2. -ios_gl_proc :: (name: [*]u8) -> *void { +// callconv(.c) so this is callable from `load_gl`'s C-conv proc-loader slot. +ios_gl_proc :: (name: [*]u8) -> *void callconv(.c) { rtld_default : *void = xx (0 - 2); dlsym(rtld_default, name); } diff --git a/src/ir/lower.zig b/src/ir/lower.zig index c206eef..2fb0ad8 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -796,7 +796,13 @@ pub const Lowering = struct { }) catch unreachable; } - const cc: Function.CallingConvention = if (fd.call_conv == .c) .c else .default; + // `#foreign` declarations are external C symbols by definition — + // promote them to callconv(.c) when the user didn't write it + // explicitly. This keeps fn-ptr coercion type-safe: anything + // typed by name as `(args) -> ret` of a `#foreign` decl can be + // assigned to / passed as a `callconv(.c)` fn-pointer without a + // call-convention mismatch. + const cc: Function.CallingConvention = if (fd.call_conv == .c or is_foreign) .c else .default; // For #foreign with C name override, declare under C name and map sx name → C name if (is_foreign) { @@ -1968,6 +1974,24 @@ pub const Lowering = struct { const tramp_id = self.createBareFnTrampoline(fid, tt_info.closure); break :blk self.builder.closureCreate(tramp_id, Ref.none, tt); } + // Coercing a bare fn name to a fn-pointer + // type — the call_conv must match. A + // default-conv sx fn assigned to a + // callconv(.c) slot (e.g. passed to + // pthread_create) would otherwise crash at + // runtime when the C caller doesn't supply + // the implicit __sx_ctx arg. + if (tt_info == .function) { + const func_cc = self.module.functions.items[@intFromEnum(fid)].call_conv; + if (func_cc != tt_info.function.call_conv) { + if (self.diagnostics) |d| { + const want_cc = if (tt_info.function.call_conv == .c) "callconv(.c)" else "default sx convention"; + const have_cc = if (func_cc == .c) "callconv(.c)" else "default sx convention"; + d.addFmt(.err, node.span, "call-convention mismatch: '{s}' is declared with {s} but the target type expects {s}", .{ eff_fn_name, have_cc, want_cc }); + } + break :blk self.emitPlaceholder(eff_fn_name); + } + } } } break :blk self.builder.emit(.{ .func_ref = fid }, .s64); diff --git a/tests/expected/131-callconv-mismatch-diagnostic.exit b/tests/expected/131-callconv-mismatch-diagnostic.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/131-callconv-mismatch-diagnostic.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/131-callconv-mismatch-diagnostic.txt b/tests/expected/131-callconv-mismatch-diagnostic.txt new file mode 100644 index 0000000..3a0c6ee --- /dev/null +++ b/tests/expected/131-callconv-mismatch-diagnostic.txt @@ -0,0 +1 @@ +/Users/agra/projects/sx/examples/131-callconv-mismatch-diagnostic.sx:12:42: error: call-convention mismatch: 'sx_handler' is declared with default sx convention but the target type expects callconv(.c) diff --git a/tests/expected/ffi-jni-call-03-methodid-sharing.ir b/tests/expected/ffi-jni-call-03-methodid-sharing.ir index 6f325ee..64695f0 100644 --- a/tests/expected/ffi-jni-call-03-methodid-sharing.ir +++ b/tests/expected/ffi-jni-call-03-methodid-sharing.ir @@ -334,5 +334,3 @@ declare ptr @sx_jni_env_tl_get() #0 declare void @sx_jni_env_tl_set(ptr) #0 declare i64 @write(i32, ptr, i64) - - diff --git a/tests/expected/ffi-jni-call-04-jint-return.ir b/tests/expected/ffi-jni-call-04-jint-return.ir index f8ada9c..6cccf98 100644 --- a/tests/expected/ffi-jni-call-04-jint-return.ir +++ b/tests/expected/ffi-jni-call-04-jint-return.ir @@ -309,5 +309,3 @@ declare ptr @sx_jni_env_tl_get() #0 declare void @sx_jni_env_tl_set(ptr) #0 declare i64 @write(i32, ptr, i64) - - diff --git a/tests/expected/ffi-jni-call-05-jlong-return.ir b/tests/expected/ffi-jni-call-05-jlong-return.ir index 4f60316..a51fc87 100644 --- a/tests/expected/ffi-jni-call-05-jlong-return.ir +++ b/tests/expected/ffi-jni-call-05-jlong-return.ir @@ -309,5 +309,3 @@ declare ptr @sx_jni_env_tl_get() #0 declare void @sx_jni_env_tl_set(ptr) #0 declare i64 @write(i32, ptr, i64) - - diff --git a/tests/expected/ffi-jni-call-06-jdouble-return.ir b/tests/expected/ffi-jni-call-06-jdouble-return.ir index c99f714..d7896e4 100644 --- a/tests/expected/ffi-jni-call-06-jdouble-return.ir +++ b/tests/expected/ffi-jni-call-06-jdouble-return.ir @@ -309,5 +309,3 @@ declare ptr @sx_jni_env_tl_get() #0 declare void @sx_jni_env_tl_set(ptr) #0 declare i64 @write(i32, ptr, i64) - - diff --git a/tests/expected/ffi-jni-call-07-jboolean-return.ir b/tests/expected/ffi-jni-call-07-jboolean-return.ir index c7a5226..3b4a152 100644 --- a/tests/expected/ffi-jni-call-07-jboolean-return.ir +++ b/tests/expected/ffi-jni-call-07-jboolean-return.ir @@ -309,5 +309,3 @@ declare ptr @sx_jni_env_tl_get() #0 declare void @sx_jni_env_tl_set(ptr) #0 declare i64 @write(i32, ptr, i64) - - diff --git a/tests/expected/ffi-jni-call-08-jobject-return.ir b/tests/expected/ffi-jni-call-08-jobject-return.ir index 678dd62..4bbfc0d 100644 --- a/tests/expected/ffi-jni-call-08-jobject-return.ir +++ b/tests/expected/ffi-jni-call-08-jobject-return.ir @@ -309,5 +309,3 @@ declare ptr @sx_jni_env_tl_get() #0 declare void @sx_jni_env_tl_set(ptr) #0 declare i64 @write(i32, ptr, i64) - - diff --git a/tests/expected/ffi-jni-call-09-static.ir b/tests/expected/ffi-jni-call-09-static.ir index 4689968..a3b6dc9 100644 --- a/tests/expected/ffi-jni-call-09-static.ir +++ b/tests/expected/ffi-jni-call-09-static.ir @@ -306,5 +306,3 @@ declare ptr @sx_jni_env_tl_get() #0 declare void @sx_jni_env_tl_set(ptr) #0 declare i64 @write(i32, ptr, i64) - - diff --git a/tests/expected/ffi-jni-class-08-call.ir b/tests/expected/ffi-jni-class-08-call.ir index d1011c6..a5bb712 100644 --- a/tests/expected/ffi-jni-class-08-call.ir +++ b/tests/expected/ffi-jni-class-08-call.ir @@ -309,5 +309,3 @@ declare ptr @sx_jni_env_tl_get() #0 declare void @sx_jni_env_tl_set(ptr) #0 declare i64 @write(i32, ptr, i64) - - diff --git a/tests/expected/ffi-jni-env-02-lexical-direct.ir b/tests/expected/ffi-jni-env-02-lexical-direct.ir index f0b1f08..91bcf8e 100644 --- a/tests/expected/ffi-jni-env-02-lexical-direct.ir +++ b/tests/expected/ffi-jni-env-02-lexical-direct.ir @@ -307,5 +307,3 @@ declare ptr @sx_jni_env_tl_get() #0 declare void @sx_jni_env_tl_set(ptr) #0 declare i64 @write(i32, ptr, i64) - - diff --git a/tests/expected/ffi-objc-call-03-selector-sharing.ir b/tests/expected/ffi-objc-call-03-selector-sharing.ir index 21d5867..f43a187 100644 --- a/tests/expected/ffi-objc-call-03-selector-sharing.ir +++ b/tests/expected/ffi-objc-call-03-selector-sharing.ir @@ -390,5 +390,3 @@ entry: store ptr %selN, ptr @OBJC_SELECTOR_REFERENCES_release, align 8 ret void } - - diff --git a/tests/expected/ffi-objc-call-06-sret-return.ir b/tests/expected/ffi-objc-call-06-sret-return.ir index 2e9b97f..eb8593d 100644 --- a/tests/expected/ffi-objc-call-06-sret-return.ir +++ b/tests/expected/ffi-objc-call-06-sret-return.ir @@ -3281,5 +3281,3 @@ entry: store ptr %sel, ptr @OBJC_SELECTOR_REFERENCES_tripleValue, align 8 ret void } - -