ffi M4.0c: -dealloc frees state through captured __sx_allocator
The synthesized -dealloc IMP now loads `state->__sx_allocator` (the
slot captured at +alloc time by M4.0a + M4.0b) and dispatches
`allocator.dealloc(state)` through the inline-protocol fn-ptr at
slot 2. Old behaviour was `free(state)` — went straight to libc,
ignoring whatever allocator the instance was constructed with.
After this commit, the per-instance allocator design from M1.2 A.5
is finally end-to-end correct:
push Context.{ allocator = arena } {
f := SxFoo.alloc(); ← arena.alloc(STATE_SIZE) + capture
// ... use f ...
}
// refcount → 0 ⇒ -dealloc:
// load state->__sx_allocator = arena
// arena.dealloc(state) ← same allocator round-trips
TrackingAllocator now sees the alloc/dealloc pair; the deferred M1.2
A.5 work is done. Closes the loop on M4.0.
The dealloc IMP passes `__sx_default_context` as the implicit __sx_ctx
when invoking the dealloc fn-ptr — the IMP itself has no caller-side
ctx (it's called by Apple's runtime at refcount-zero), and the
default GPA is the right baseline for any nested allocations the
dealloc body might perform.
Each compiler-internal lookup that "can't fail" (Context type,
__sx_default_context global) emits a loud diagnostic instead of
silent fall-through, per the silent-error budget.
184/184 example tests pass; chess on iOS-sim green.
This commit is contained in:
@@ -12607,27 +12607,22 @@ pub const Lowering = struct {
|
||||
}
|
||||
|
||||
/// Synthesize the `-dealloc` IMP for an sx-defined `#objc_class`.
|
||||
/// Runs when the Obj-C runtime drops the last retain on an
|
||||
/// instance.
|
||||
/// Runs when the Obj-C runtime drops the last retain on an instance.
|
||||
///
|
||||
/// C-ABI: `(self: id, _cmd: SEL) -> void`
|
||||
/// C-ABI: `(self: id, _cmd: SEL) -> void`. No implicit sx ctx.
|
||||
///
|
||||
/// Body:
|
||||
/// %state = object_getIvar(self, load @__<Cls>_state_ivar)
|
||||
/// free(state)
|
||||
/// Body (M4.0c):
|
||||
/// %state = object_getIvar(self, load @__<Cls>_state_ivar)
|
||||
/// %allocator = load struct_gep(state, 0) ← __sx_allocator (M4.0a)
|
||||
/// allocator.dealloc(state) ← via inline-protocol fn-ptr
|
||||
/// object_setIvar(self, ivar, null)
|
||||
/// // [super dealloc] via objc_msgSendSuper2(&super, sel_dealloc)
|
||||
/// %sup = alloca { *void, *void }
|
||||
/// store self into sup.0 (receiver)
|
||||
/// store @__<Cls>_class into sup.1 (current class — runtime climbs)
|
||||
/// %sel_dealloc = sel_registerName("dealloc")
|
||||
/// objc_msgSendSuper2(%sup, %sel_dealloc)
|
||||
/// [super dealloc] // objc_msgSendSuper2(&super, sel_dealloc)
|
||||
/// ret void
|
||||
///
|
||||
/// `free(null)` is well-defined as no-op per C standard, so we
|
||||
/// skip the null check. The state-ivar nil-out prevents UAF if
|
||||
/// super-dealloc somehow re-reads our ivar (paranoia — NSObject
|
||||
/// doesn't).
|
||||
/// The state struct's first field is the allocator captured at
|
||||
/// +alloc time (M4.0a + M4.0b). Reading it back lets -dealloc free
|
||||
/// through the same allocator the instance was constructed with —
|
||||
/// the per-instance allocator design from M1.2 A.5, now realised.
|
||||
fn emitObjcDefinedClassDeallocImp(self: *Lowering, fcd: *const ast.ForeignClassDecl) void {
|
||||
const saved_func = self.builder.func;
|
||||
const saved_block = self.builder.current_block;
|
||||
@@ -12672,11 +12667,52 @@ pub const Lowering = struct {
|
||||
get_args[1] = ivar_handle;
|
||||
const state = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void);
|
||||
|
||||
// (2) free(state) — free(NULL) is a safe no-op.
|
||||
const free_fid = self.ensureCRuntimeDecl("free", &.{ptr_void}, .void);
|
||||
const free_args = self.alloc.alloc(Ref, 1) catch return;
|
||||
free_args[0] = state;
|
||||
_ = self.builder.emit(.{ .call = .{ .callee = free_fid, .args = free_args } }, .void);
|
||||
// (2) Free state through the captured allocator (M4.0a + M4.0b):
|
||||
// allocator = load struct_gep(state, 0) ← __sx_allocator field
|
||||
// allocator.dealloc(state) ← inline-protocol fn-ptr at field 2
|
||||
// Compare to the old `free(state)` — that ignored the per-instance
|
||||
// allocator and went straight to libc. Now `push Context.{ allocator = arena }`
|
||||
// round-trips correctly: arena.alloc on construction, arena.dealloc here.
|
||||
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassDeallocImp: Context type not found for class '{s}' (compiler bug)", .{fcd.name});
|
||||
}
|
||||
return;
|
||||
};
|
||||
const ctx_info = self.module.types.get(ctx_ty);
|
||||
if (ctx_info != .@"struct" or ctx_info.@"struct".fields.len < 1) {
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassDeallocImp: Context has unexpected shape for class '{s}'", .{fcd.name});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const allocator_ty = ctx_info.@"struct".fields[0].ty;
|
||||
|
||||
const state_struct_ty = self.objcDefinedStateStructType(fcd);
|
||||
const state_alloc_addr = self.builder.emit(.{ .struct_gep = .{
|
||||
.base = state,
|
||||
.field_index = 0,
|
||||
.base_type = state_struct_ty,
|
||||
} }, ptr_void);
|
||||
const allocator = self.builder.load(state_alloc_addr, allocator_ty);
|
||||
|
||||
// Default-context address for the implicit __sx_ctx the dealloc
|
||||
// fn-ptr takes as its first arg (the dealloc body might allocate
|
||||
// internally; default GPA is the safe baseline).
|
||||
const default_ctx_gi = self.global_names.get("__sx_default_context") orelse {
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassDeallocImp: __sx_default_context global missing for class '{s}'", .{fcd.name});
|
||||
}
|
||||
return;
|
||||
};
|
||||
const default_ctx_addr = self.builder.emit(.{ .global_addr = default_ctx_gi.id }, ptr_void);
|
||||
const alloc_ctx = self.builder.structGet(allocator, 0, ptr_void);
|
||||
const dealloc_fn_ptr = self.builder.structGet(allocator, 2, ptr_void);
|
||||
const dealloc_args = self.alloc.dupe(Ref, &.{ default_ctx_addr, alloc_ctx, state }) catch return;
|
||||
_ = self.builder.emit(.{ .call_indirect = .{
|
||||
.callee = dealloc_fn_ptr,
|
||||
.args = dealloc_args,
|
||||
} }, .void);
|
||||
|
||||
// (3) object_setIvar(self, ivar, null)
|
||||
const set_ivar_fid = self.ensureCRuntimeDecl("object_setIvar", &.{ ptr_void, ptr_void, ptr_void }, .void);
|
||||
|
||||
Reference in New Issue
Block a user