From 183c835c22f0128b30a7d37c834a97955b7002f1 Mon Sep 17 00:00:00 2001 From: "Attah N." Date: Sun, 26 Jul 2026 07:59:01 +0100 Subject: [PATCH 1/4] Add typed native handle drops --- CHANGELOG.md | 14 +- docs/ROADMAP.md | 2 +- .../native_owned_handle_drop_test.mko | 64 +++ runtime/native_bridge.c | 93 +++- runtime/native_runtime.c | 342 +++++++++++- scripts/memory-safety-gate.sh | 1 + src/llvm_codegen.rs | 40 +- src/native_codegen.rs | 33 ++ src/native_ir.rs | 502 ++++++++++++++---- 9 files changed, 951 insertions(+), 140 deletions(-) create mode 100644 examples/native/owned_handle_drop/native_owned_handle_drop_test.mko diff --git a/CHANGELOG.md b/CHANGELOG.md index 24240958..bd820f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,13 +115,13 @@ exports `free_string`, so it stays borrowed; the aliasing string-array helpers share buffers, so freeing both input and output would be a double free; `sctp_connectx` passes a non-packed address array. -`mako_native_opaque_drop` is now a no-op, which trades a heap corruption for a -leak: `Type::Opaque` covers both blocks this runtime allocated and handles owned -by a foreign library, and nothing at runtime distinguishes them, so the boxes in -the first group are no longer reclaimed. Emitting a typed drop from codegen -would let it free the ones it owns again and would also close the leaks in the -`http_request_parse` opaque box and in struct-key map drops, which have the same -root cause. +**Native typed drops.** The shared IR now distinguishes runtime-owned opaque +values from foreign handles. Interface boxes are reference-counted, +`http_request_parse` results are deep-cloned and dropped, and foreign database, +registry, and OS handles remain on their explicit close paths. Content-keyed +struct maps also clone and release their owned keys and values on read, +overwrite, delete, copy, clear, and final drop. The focused native fixture and +the full struct-key map suite pass under LeakSanitizer and AddressSanitizer. ### General networking primitives (protocol-agnostic) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c019b8d1..7bc23878 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -245,7 +245,7 @@ hiding failures. Ordered by leverage, not by size. | **417-B** | Windows gates CI | The whole Windows suite runs under `continue-on-error`, so a platform that ships release artifacts and install scripts never blocks a merge. Establish whether it currently passes, then gate it. | | **417-C** | Sanitizer claims state their coverage | CI's ASan and UBSan jobs already run all 392 fixtures — that is how the `slices_unique_strs` use-after-free was caught. The gap was in reporting: an ad-hoc sweep covered 113 fixtures and was described as "clean". A sanitizer result carries the number of fixtures it ran. | | **417-D** | Native LeakSanitizer step executes | **Done** — the test binary links the runtime (169 symbols), the four gate fixtures pass, and a positive control leaks 44,000 bytes in 1,000 allocations, so the step fails when there is something to find. The leak it detects is 417-E's. | -| **417-E** | Typed drop for opaque handles | One change closes three leaks — the opaque box, `http_request_parse`, and struct-key map drops — which share a root cause: one drop function for two kinds of handle with nothing to tell them apart. Lets `mako_native_opaque_drop` stop being a no-op. | +| **417-E** | Typed drop for opaque handles | **Done** — native IR distinguishes runtime-owned interface and HTTP-request boxes from foreign handles, emits their typed clone/drop paths, and keeps explicit-close handles borrowed. Struct-key maps now clone and release owned keys and values across lookup, range, overwrite, delete, copy, clear, and final drop. | | **417-F** | Triage `sip_test` | 1.54 MB across 140k allocations, the largest single leak in the suite and never investigated. Measure before assuming a cause. | | **417-G** | Literal arguments at builtin call sites | 4-13 bytes per call. Needs a shared argument-emission path; builtin arms are spread across ~100 generated emitters with no common place to reclaim a temporary. Emitting borrowed views frees static storage; an immortal bit needs masking every length read. Design decision first. | | **417-H** | Channel lifetime | No destructor exists, and a naive one is a use-after-free when a spawned task outlives the creating scope. Needs a refcount or a shutdown-drained registry — a decision, not a patch. Blocks scheduler work. | diff --git a/examples/native/owned_handle_drop/native_owned_handle_drop_test.mko b/examples/native/owned_handle_drop/native_owned_handle_drop_test.mko new file mode 100644 index 00000000..cdc6e7d6 --- /dev/null +++ b/examples/native/owned_handle_drop/native_owned_handle_drop_test.mko @@ -0,0 +1,64 @@ +struct Counter { + label: string + n: int +} + +interface Adder { + fn add(int) -> int +} + +struct AdderHolder { + value: Adder +} + +struct RequestHolder { + value: HttpRequest +} + +fn Adder_add(self: Counter, delta: int) -> int { + return self.n + delta +} + +struct Label { + text: string + id: int +} + +fn interface_once(n: int) -> int { + let counter = Counter { label: int_to_string(n), n: n } + let adder: Adder = counter + let first = AdderHolder { value: adder } + let second = first + return first.value.add(1) + second.value.add(1) +} + +fn request_once() -> int { + let raw = "POST /items HTTP/1.1\r\nHost: example.test\r\n\r\nbody" + let request = http_request_parse(raw) + let first = RequestHolder { value: request } + let second = first + return len(http_request_method(first.value)) + len(http_request_path(second.value)) +} + +fn struct_key_map_once(n: int) -> int { + let text = int_to_string(n) + let mut values = make(map[Label]Label) + values[Label { text: text, id: n }] = Label { + text: int_to_string(n + 1), + id: n + 1, + } + let copy = maps_clone(values) + return copy[Label { text: text, id: n }].id +} + +fn TestOwnedNativeHandlesDrop() { + let mut i = 0 + let mut total = 0 + while i < 200 { + total = total + interface_once(i) + total = total + request_once() + total = total + struct_key_map_once(i) + i = i + 1 + } + assert(total > 0) +} diff --git a/runtime/native_bridge.c b/runtime/native_bridge.c index eea0dacb..1a80102c 100644 --- a/runtime/native_bridge.c +++ b/runtime/native_bridge.c @@ -1039,29 +1039,6 @@ MakoNativeIntSlice *mako_native_int_map_apply(MakoNativeIntSlice *src, void *fn_ return out; } -/* Type::Opaque covers two unrelated kinds of handle, and nothing at runtime - * distinguishes them: - * - * 1. blocks this runtime malloc'd (e.g. the box behind http_request_parse), - * which free() would correctly reclaim; - * 2. handles owned by a foreign library or by one of our registries — a - * sqlite3* from mako_sql_open_sqlite, a TlsConn — which must be released - * through their own API (sqlite3_close, tls_conn_close) and must never be - * passed to free(). - * - * The SqlDB value handed to Mako code is `(int64_t)(intptr_t)db.sqlite` - * (mako_sql_meta_key), so a bare free() here corrupts the heap on every scope - * exit that holds a database handle: glibc aborts with "free(): invalid - * pointer", and macOS silently tolerates it, which is why this only ever - * reproduced on Linux. - * - * Until codegen emits a typed drop that knows which kind it has, do nothing. - * Kind 2 must not be freed at all; kind 1 leaks its box, which is recoverable. - * Corrupting the allocator is not. */ -void mako_native_opaque_drop(void *p) { - (void)p; -} - // parse_int → try: returns 1 and writes *out on success, else 0. int64_t mako_native_parse_int_try_ptr(MakoNativeString *s, int64_t *out) { MakoResultInt r = mako_parse_int(bridge_borrow_str(s)); @@ -1737,20 +1714,46 @@ int64_t mako_native_http2_conn_new(void) { return c ? (int64_t)(intptr_t)c : -1; } -/* Interface fat pointer: { tag, data* } */ +void mako_native_struct_drop_typed( + void *value, int64_t nfields, int64_t str_mask, + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack +); + +/* Interface fat pointer with the concrete value's drop metadata. */ typedef struct { int64_t tag; void *data; + int64_t nfields; + int64_t str_mask; + int64_t nest_mask; + int64_t nest_nf_pack; + int64_t nest_sm_pack; + _Atomic size_t refs; } MakoNativeIface; -void *mako_native_iface_box(int64_t tag, void *data) { +void *mako_native_iface_box( + int64_t tag, void *data, int64_t nfields, int64_t str_mask, + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack +) { MakoNativeIface *b = (MakoNativeIface *)malloc(sizeof(MakoNativeIface)); if (!b) abort(); b->tag = tag; b->data = data; /* takes ownership of struct heap block */ + b->nfields = nfields; + b->str_mask = str_mask; + b->nest_mask = nest_mask; + b->nest_nf_pack = nest_nf_pack; + b->nest_sm_pack = nest_sm_pack; + atomic_init(&b->refs, 1); return b; } +void *mako_native_iface_clone(void *box) { + MakoNativeIface *b = (MakoNativeIface *)box; + if (b) atomic_fetch_add_explicit(&b->refs, 1, memory_order_relaxed); + return box; +} + int64_t mako_native_iface_tag(void *box) { return box ? ((MakoNativeIface *)box)->tag : 0; } @@ -1759,9 +1762,16 @@ void *mako_native_iface_data(void *box) { return box ? ((MakoNativeIface *)box)->data : NULL; } -/* opaque_drop already frees; iface box free frees header only if data freed separately. - * For owned iface drop: free data via struct drop then free box — IR uses opaque_drop - * which free(box) only. Leak of struct data — acceptable seed; improve later. */ +void mako_native_iface_drop(void *box) { + MakoNativeIface *b = (MakoNativeIface *)box; + if (!b) return; + if (atomic_fetch_sub_explicit(&b->refs, 1, memory_order_acq_rel) != 1) return; + mako_native_struct_drop_typed( + b->data, b->nfields, b->str_mask, + b->nest_mask, b->nest_nf_pack, b->nest_sm_pack + ); + free(box); +} int64_t mako_native_http2_conn_use(int64_t c) { return mako_http2_conn_use((MakoHttp2Conn *)(intptr_t)c); @@ -8507,6 +8517,19 @@ typedef struct { MakoNativeString *path; MakoNativeString *body; } MakoNativeHttpRequest; + +static MakoNativeHttpRequest *mako_native_http_request_clone( + const MakoNativeHttpRequest *src +) { + if (!src) return NULL; + MakoNativeHttpRequest *box = (MakoNativeHttpRequest *)calloc(1, sizeof(*box)); + if (!box) abort(); + box->method = mako_native_string_clone_ptr(src->method); + box->path = mako_native_string_clone_ptr(src->path); + box->body = mako_native_string_clone_ptr(src->body); + return box; +} + int64_t mako_native_http_request_parse_ptr(MakoNativeString *raw) { MakoHttpRequest r = mako_http_request_parse(bridge_borrow_str(raw)); MakoNativeHttpRequest *box = (MakoNativeHttpRequest *)calloc(1, sizeof(*box)); @@ -8516,6 +8539,19 @@ int64_t mako_native_http_request_parse_ptr(MakoNativeString *raw) { box->body = bridge_take_str(mako_http_request_body(r)); return (int64_t)(intptr_t)box; } +int64_t mako_native_http_request_clone_ptr(int64_t h) { + return (int64_t)(intptr_t)mako_native_http_request_clone( + (MakoNativeHttpRequest *)(intptr_t)h + ); +} +void mako_native_http_request_drop_ptr(int64_t h) { + MakoNativeHttpRequest *box = (MakoNativeHttpRequest *)(intptr_t)h; + if (!box) return; + mako_native_string_drop_ptr(box->method); + mako_native_string_drop_ptr(box->path); + mako_native_string_drop_ptr(box->body); + free(box); +} MakoNativeString *mako_native_http_request_method_ptr(int64_t h) { MakoNativeHttpRequest *b = (MakoNativeHttpRequest *)(intptr_t)h; if (!b || !b->method) return mako_native_string_literal_ptr("", 0); @@ -10816,4 +10852,3 @@ int64_t mako_native_peer_table_capacity(int64_t a0) { int64_t mako_native_peer_table_alive(int64_t a0, int64_t a1) { return mako_peer_table_alive(a0, a1); } - diff --git a/runtime/native_runtime.c b/runtime/native_runtime.c index d142f008..fe6cbcd4 100644 --- a/runtime/native_runtime.c +++ b/runtime/native_runtime.c @@ -1612,6 +1612,193 @@ static void mako_native_struct_key_free( free(key); } +void mako_native_struct_drop_typed( + void *value, int64_t nfields, int64_t str_mask, + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack +) { + mako_native_struct_key_free( + value, nfields, str_mask, nest_mask, nest_nf_pack, nest_sm_pack + ); +} + +typedef struct { + void **data; + size_t len; + size_t cap; + int64_t owned; +} MakoNativeStructSlice; + +void *mako_native_iface_clone(void *box); +void mako_native_iface_drop(void *box); +int64_t mako_native_http_request_clone_ptr(int64_t handle); +void mako_native_http_request_drop_ptr(int64_t handle); + +MakoNativeStructSlice *mako_native_struct_slice_clone_ptr( + const MakoNativeStructSlice *in, int64_t nfields, int64_t str_mask, + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack +) { + if (!in) return NULL; + MakoNativeStructSlice *out = calloc(1, sizeof(*out)); + if (!out) abort(); + out->data = calloc(in->len, sizeof(*out->data)); + if (in->len && !out->data) abort(); + out->len = in->len; + out->cap = in->len; + out->owned = 1; + for (size_t i = 0; i < in->len; ++i) { + out->data[i] = mako_native_struct_key_clone( + in->data[i], nfields, str_mask, nest_mask, nest_nf_pack, nest_sm_pack + ); + } + return out; +} + +void mako_native_struct_slice_drop_ptr( + MakoNativeStructSlice *slice, int64_t nfields, int64_t str_mask, + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack +) { + if (!slice) return; + if (slice->owned && slice->data) { + for (size_t i = 0; i < slice->len; ++i) { + mako_native_struct_key_free( + slice->data[i], nfields, str_mask, + nest_mask, nest_nf_pack, nest_sm_pack + ); + } + free(slice->data); + } + free(slice); +} + +/* Value kinds for content-keyed maps: + * 0 scalar/shared handle, 1 string, 2 struct, 3 []int, 4 []string, + * 5 []float, 6 []struct, 7 interface, 8 HTTP request. */ +static void mako_native_struct_map_value_drop( + int64_t value, int64_t kind, int64_t nfields, int64_t str_mask, + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack +) { + if (!value) return; + switch (kind) { + case 1: + mako_native_string_drop_ptr((MakoNativeString *)(intptr_t)value); + break; + case 2: + mako_native_struct_key_free( + (void *)(intptr_t)value, nfields, str_mask, + nest_mask, nest_nf_pack, nest_sm_pack + ); + break; + case 3: + mako_native_int_slice_drop_ptr((MakoNativeIntSlice *)(intptr_t)value); + break; + case 4: + mako_native_string_slice_drop_ptr((MakoNativeStringSlice *)(intptr_t)value); + break; + case 5: + mako_native_float_slice_drop_ptr((MakoNativeFloatSlice *)(intptr_t)value); + break; + case 6: { + mako_native_struct_slice_drop_ptr( + (MakoNativeStructSlice *)(intptr_t)value, nfields, str_mask, + nest_mask, nest_nf_pack, nest_sm_pack + ); + break; + } + case 7: + mako_native_iface_drop((void *)(intptr_t)value); + break; + case 8: + mako_native_http_request_drop_ptr(value); + break; + default: + break; + } +} + +static int64_t mako_native_struct_map_value_clone( + int64_t value, int64_t kind, int64_t nfields, int64_t str_mask, + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack +) { + if (!value) return 0; + switch (kind) { + case 1: + return (int64_t)(intptr_t)mako_native_string_clone_ptr( + (MakoNativeString *)(intptr_t)value + ); + case 2: + return (int64_t)(intptr_t)mako_native_struct_key_clone( + (void *)(intptr_t)value, nfields, str_mask, + nest_mask, nest_nf_pack, nest_sm_pack + ); + case 3: + return (int64_t)(intptr_t)mako_native_int_slice_clone_ptr( + (MakoNativeIntSlice *)(intptr_t)value + ); + case 4: + return (int64_t)(intptr_t)mako_native_string_slice_clone_ptr( + (MakoNativeStringSlice *)(intptr_t)value + ); + case 5: + return (int64_t)(intptr_t)mako_native_float_slice_clone_ptr( + (MakoNativeFloatSlice *)(intptr_t)value + ); + case 6: { + return (int64_t)(intptr_t)mako_native_struct_slice_clone_ptr( + (MakoNativeStructSlice *)(intptr_t)value, nfields, str_mask, + nest_mask, nest_nf_pack, nest_sm_pack + ); + } + case 7: + return (int64_t)(intptr_t)mako_native_iface_clone( + (void *)(intptr_t)value + ); + case 8: + return mako_native_http_request_clone_ptr(value); + default: + return value; + } +} + +MakoNativeStructSlice *mako_native_ptr_slice_clone_typed( + const MakoNativeStructSlice *in, int64_t value_kind, + int64_t nfields, int64_t str_mask, int64_t nest_mask, + int64_t nest_nf_pack, int64_t nest_sm_pack +) { + if (!in) return NULL; + MakoNativeStructSlice *out = calloc(1, sizeof(*out)); + if (!out) abort(); + out->data = calloc(in->len, sizeof(*out->data)); + if (in->len && !out->data) abort(); + out->len = in->len; + out->cap = in->len; + out->owned = 1; + for (size_t i = 0; i < in->len; ++i) { + out->data[i] = (void *)(intptr_t)mako_native_struct_map_value_clone( + (int64_t)(intptr_t)in->data[i], value_kind, nfields, str_mask, + nest_mask, nest_nf_pack, nest_sm_pack + ); + } + return out; +} + +void mako_native_ptr_slice_drop_typed( + MakoNativeStructSlice *slice, int64_t value_kind, + int64_t nfields, int64_t str_mask, int64_t nest_mask, + int64_t nest_nf_pack, int64_t nest_sm_pack +) { + if (!slice) return; + if (slice->owned && slice->data) { + for (size_t i = 0; i < slice->len; ++i) { + mako_native_struct_map_value_drop( + (int64_t)(intptr_t)slice->data[i], value_kind, nfields, str_mask, + nest_mask, nest_nf_pack, nest_sm_pack + ); + } + free(slice->data); + } + free(slice); +} + /* Struct keys bucket by content hash, so growth cannot reuse * mako_native_map_ii_rehash — that re-buckets by the raw key-pointer word and * strands every entry where lookups can no longer find it. Move the already @@ -1628,6 +1815,7 @@ static void mako_native_map_struct_key_rehash( while (cap < ncap) cap *= 2; if (cap < ocap * 2) cap = ocap * 2; mako_native_map_ii_alloc_tables(m, cap); + m->tombs = 0; for (size_t i = 0; i < ocap; ++i) { if (ostate[i] != MAKO_NMAP_FULL) continue; int64_t hk = mako_native_struct_content_key( @@ -1648,10 +1836,12 @@ static void mako_native_map_struct_key_rehash( void mako_native_map_struct_key_set_ptr( MakoNativeMapII *m, void *key, int64_t nfields, int64_t str_mask, - int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack, int64_t val + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack, int64_t val, + int64_t val_kind, int64_t val_nfields, int64_t val_str_mask, + int64_t val_nest_mask, int64_t val_nest_nf_pack, int64_t val_nest_sm_pack ) { if (!m || !key || !m->lenp) abort(); - if (*m->lenp >= mako_native_map_ii_grow_at(m->cap)) { + if (*m->lenp + m->tombs >= mako_native_map_ii_grow_at(m->cap)) { mako_native_map_struct_key_rehash( m, m->cap * 2, nfields, str_mask, nest_mask, nest_nf_pack, nest_sm_pack ); @@ -1667,6 +1857,7 @@ void mako_native_map_struct_key_set_ptr( for (size_t n = 0; n < m->cap; ++n) { if (state[i] == MAKO_NMAP_EMPTY) { size_t slot = first_tomb != (size_t)-1 ? first_tomb : i; + if (first_tomb != (size_t)-1 && m->tombs) m->tombs--; state[slot] = MAKO_NMAP_FULL; keys[slot] = (int64_t)(intptr_t)mako_native_struct_key_clone( key, nfields, str_mask, nest_mask, nest_nf_pack, nest_sm_pack @@ -1681,6 +1872,10 @@ void mako_native_map_struct_key_set_ptr( if (mako_native_struct_key_eq( exist, key, nfields, str_mask, nest_mask, nest_nf_pack, nest_sm_pack )) { + mako_native_struct_map_value_drop( + vals[i], val_kind, val_nfields, val_str_mask, + val_nest_mask, val_nest_nf_pack, val_nest_sm_pack + ); vals[i] = val; return; } @@ -1743,11 +1938,14 @@ int64_t mako_native_map_struct_key_has_ptr( void mako_native_map_struct_key_delete_ptr( MakoNativeMapII *m, void *key, int64_t nfields, int64_t str_mask, - int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack, + int64_t val_kind, int64_t val_nfields, int64_t val_str_mask, + int64_t val_nest_mask, int64_t val_nest_nf_pack, int64_t val_nest_sm_pack ) { if (!m || !m->cap || !m->state || !key) return; uint8_t *state = mako_native_map_ii_state(m); int64_t *keys = mako_native_map_ii_keys(m); + int64_t *vals = mako_native_map_ii_vals(m); int64_t hk = mako_native_struct_content_key( key, nfields, str_mask, nest_mask, nest_nf_pack, nest_sm_pack ); @@ -1762,9 +1960,15 @@ void mako_native_map_struct_key_delete_ptr( mako_native_struct_key_free( exist, nfields, str_mask, nest_mask, nest_nf_pack, nest_sm_pack ); + mako_native_struct_map_value_drop( + vals[i], val_kind, val_nfields, val_str_mask, + val_nest_mask, val_nest_nf_pack, val_nest_sm_pack + ); keys[i] = 0; + vals[i] = 0; state[i] = MAKO_NMAP_TOMB; (*m->lenp)--; + m->tombs++; return; } } @@ -1775,7 +1979,9 @@ void mako_native_map_struct_key_delete_ptr( /* Clone / copy / equal for content-keyed struct maps. */ MakoNativeMapII *mako_native_map_struct_key_clone_ptr( const MakoNativeMapII *m, int64_t nfields, int64_t str_mask, - int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack, + int64_t val_kind, int64_t val_nfields, int64_t val_str_mask, + int64_t val_nest_mask, int64_t val_nest_nf_pack, int64_t val_nest_sm_pack ) { if (!m) return NULL; size_t len = mako_native_map_ii_len(m); @@ -1786,8 +1992,14 @@ MakoNativeMapII *mako_native_map_struct_key_clone_ptr( for (size_t i = 0; i < m->cap; ++i) { if (state[i] == MAKO_NMAP_FULL) { void *k = (void *)(intptr_t)keys[i]; + int64_t value = mako_native_struct_map_value_clone( + vals[i], val_kind, val_nfields, val_str_mask, + val_nest_mask, val_nest_nf_pack, val_nest_sm_pack + ); mako_native_map_struct_key_set_ptr( - n, k, nfields, str_mask, nest_mask, nest_nf_pack, nest_sm_pack, vals[i] + n, k, nfields, str_mask, nest_mask, nest_nf_pack, nest_sm_pack, value, + val_kind, val_nfields, val_str_mask, + val_nest_mask, val_nest_nf_pack, val_nest_sm_pack ); } } @@ -1796,22 +2008,31 @@ MakoNativeMapII *mako_native_map_struct_key_clone_ptr( void mako_native_maps_copy_struct_key( MakoNativeMapII *dst, const MakoNativeMapII *src, int64_t nfields, int64_t str_mask, - int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack, + int64_t val_kind, int64_t val_nfields, int64_t val_str_mask, + int64_t val_nest_mask, int64_t val_nest_nf_pack, int64_t val_nest_sm_pack ) { if (dst && dst->state) { uint8_t *st = mako_native_map_ii_state(dst); int64_t *dk = mako_native_map_ii_keys(dst); + int64_t *dv = mako_native_map_ii_vals(dst); for (size_t i = 0; i < dst->cap; ++i) { if (st[i] == MAKO_NMAP_FULL) { mako_native_struct_key_free( (void *)(intptr_t)dk[i], nfields, str_mask, nest_mask, nest_nf_pack, nest_sm_pack ); + mako_native_struct_map_value_drop( + dv[i], val_kind, val_nfields, val_str_mask, + val_nest_mask, val_nest_nf_pack, val_nest_sm_pack + ); dk[i] = 0; + dv[i] = 0; } st[i] = MAKO_NMAP_EMPTY; } if (dst->lenp) *dst->lenp = 0; + dst->tombs = 0; } if (!src || !dst) return; uint8_t *state = mako_native_map_ii_state(src); @@ -1820,11 +2041,43 @@ void mako_native_maps_copy_struct_key( for (size_t i = 0; i < src->cap; ++i) { if (state[i] == MAKO_NMAP_FULL) { void *k = (void *)(intptr_t)keys[i]; + int64_t value = mako_native_struct_map_value_clone( + vals[i], val_kind, val_nfields, val_str_mask, + val_nest_mask, val_nest_nf_pack, val_nest_sm_pack + ); mako_native_map_struct_key_set_ptr( - dst, k, nfields, str_mask, nest_mask, nest_nf_pack, nest_sm_pack, vals[i] + dst, k, nfields, str_mask, nest_mask, nest_nf_pack, nest_sm_pack, value, + val_kind, val_nfields, val_str_mask, + val_nest_mask, val_nest_nf_pack, val_nest_sm_pack + ); + } + } +} + +void mako_native_maps_clear_struct_key( + MakoNativeMapII *m, int64_t nfields, int64_t str_mask, + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack, + int64_t val_kind, int64_t val_nfields, int64_t val_str_mask, + int64_t val_nest_mask, int64_t val_nest_nf_pack, int64_t val_nest_sm_pack +) { + if (!m || !m->state) return; + for (size_t i = 0; i < m->cap; ++i) { + if (m->state[i] == MAKO_NMAP_FULL) { + mako_native_struct_key_free( + (void *)(intptr_t)m->keys[i], nfields, str_mask, + nest_mask, nest_nf_pack, nest_sm_pack + ); + mako_native_struct_map_value_drop( + m->vals[i], val_kind, val_nfields, val_str_mask, + val_nest_mask, val_nest_nf_pack, val_nest_sm_pack ); } + m->keys[i] = 0; + m->vals[i] = 0; + m->state[i] = MAKO_NMAP_EMPTY; } + if (m->lenp) *m->lenp = 0; + m->tombs = 0; } /* val_kind: 0 = i64 bits, 1 = string content, 2 = nested struct value. */ @@ -1872,6 +2125,34 @@ int64_t mako_native_maps_equal_struct_key( return 1; } +void mako_native_map_struct_key_drop_ptr( + MakoNativeMapII *m, int64_t nfields, int64_t str_mask, + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack, + int64_t val_kind, int64_t val_nfields, int64_t val_str_mask, + int64_t val_nest_mask, int64_t val_nest_nf_pack, int64_t val_nest_sm_pack +) { + if (!m) return; + if (m->state) { + for (size_t i = 0; i < m->cap; ++i) { + if (m->state[i] == MAKO_NMAP_FULL) { + mako_native_struct_key_free( + (void *)(intptr_t)m->keys[i], nfields, str_mask, + nest_mask, nest_nf_pack, nest_sm_pack + ); + mako_native_struct_map_value_drop( + m->vals[i], val_kind, val_nfields, val_str_mask, + val_nest_mask, val_nest_nf_pack, val_nest_sm_pack + ); + } + } + } + free(m->keys); + free(m->vals); + free(m->state); + free(m->lenp); + free(m); +} + void mako_native_map_ii_drop_ptr(MakoNativeMapII *m) { if (!m) return; free(m->keys); @@ -2428,6 +2709,26 @@ MakoNativeIntSlice *mako_native_maps_keys_ii_ptr(const MakoNativeMapII *m) { return out; } +MakoNativeIntSlice *mako_native_maps_keys_struct_ptr( + const MakoNativeMapII *m, int64_t nfields, int64_t str_mask, + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack +) { + int64_t n = (int64_t)mako_native_map_ii_len(m); + MakoNativeIntSlice *out = mako_native_int_slice_make_ptr((size_t)n, (size_t)n); + if (!m || !m->state || !n) return out; + size_t j = 0; + for (size_t i = 0; i < m->cap; ++i) { + if (m->state[i] == MAKO_NMAP_FULL) { + out->data[j++] = (int64_t)(intptr_t)mako_native_struct_key_clone( + (void *)(intptr_t)m->keys[i], nfields, str_mask, + nest_mask, nest_nf_pack, nest_sm_pack + ); + } + } + out->len = j; + return out; +} + // maps_keys for map[string]* → owned []string of full-slot keys. MakoNativeStringSlice *mako_native_maps_keys_si_ptr(const MakoNativeMapSI *m) { size_t n = m ? m->len : 0; @@ -2508,6 +2809,33 @@ MakoNativePtrSlice *mako_native_maps_values_ii_as_ptr_slice(const MakoNativeMapI return out; } +MakoNativeStructSlice *mako_native_maps_values_struct_key( + const MakoNativeMapII *m, int64_t val_kind, int64_t val_nfields, + int64_t val_str_mask, int64_t val_nest_mask, + int64_t val_nest_nf_pack, int64_t val_nest_sm_pack +) { + int64_t n = (int64_t)mako_native_map_ii_len(m); + MakoNativeStructSlice *out = calloc(1, sizeof(*out)); + if (!out) abort(); + out->data = calloc((size_t)n, sizeof(*out->data)); + if (n && !out->data) abort(); + out->len = (size_t)n; + out->cap = (size_t)n; + out->owned = 1; + if (!m || !m->state || !n) return out; + size_t j = 0; + for (size_t i = 0; i < m->cap; ++i) { + if (m->state[i] == MAKO_NMAP_FULL) { + out->data[j++] = (void *)(intptr_t)mako_native_struct_map_value_clone( + m->vals[i], val_kind, val_nfields, val_str_mask, + val_nest_mask, val_nest_nf_pack, val_nest_sm_pack + ); + } + } + out->len = j; + return out; +} + MakoNativeIntSlice *mako_native_maps_values_si_as_i64_ptr(const MakoNativeMapSI *m) { int64_t n = m ? (int64_t)m->len : 0; MakoNativeIntSlice *out = mako_native_int_slice_make_ptr((size_t)n, (size_t)n); diff --git a/scripts/memory-safety-gate.sh b/scripts/memory-safety-gate.sh index 4e16d2df..9bca081f 100755 --- a/scripts/memory-safety-gate.sh +++ b/scripts/memory-safety-gate.sh @@ -96,6 +96,7 @@ echo "=== memory-safety-gate: native backend under LeakSanitizer ===" if [[ "$(uname -s)" == "Linux" ]] && "$mako_bin" build --help 2>/dev/null | grep -q 'native'; then ms_native_leak_failed=0 for f in examples/testing/memory_safety_contract_test.mko \ + examples/native/owned_handle_drop/native_owned_handle_drop_test.mko \ examples/testing/leak_detector_test.mko \ examples/testing/own_branch_regress_test.mko \ examples/testing/env_vars_test.mko; do diff --git a/src/llvm_codegen.rs b/src/llvm_codegen.rs index d9323e15..2c424223 100644 --- a/src/llvm_codegen.rs +++ b/src/llvm_codegen.rs @@ -1,7 +1,7 @@ //! Optimizing LLVM emitter for backend-neutral native IR. use crate::ast::{BinOp, UnaryOp}; -use crate::native_ir::{self, Inst, Terminator, Type, Value}; +use crate::native_ir::{self, Inst, OpaqueKind, Terminator, Type, Value}; use inkwell::basic_block::BasicBlock; use inkwell::builder::Builder; use inkwell::context::Context; @@ -91,6 +91,7 @@ fn llvm_type<'ctx>(context: &'ctx Context, ty: Type) -> BasicTypeEnum<'ctx> { | Type::Nursery | Type::Task | Type::Opaque + | Type::OwnedOpaque(_) | Type::FnPtr | Type::StructSlice(_) | Type::ShareInt @@ -354,6 +355,7 @@ fn emit_instruction<'ctx>( | Type::Nursery | Type::Task | Type::Opaque + | Type::OwnedOpaque(_) | Type::FnPtr | Type::StructSlice(_) | Type::BoolSlice @@ -1718,6 +1720,24 @@ fn llvm_emit_struct_clone<'ctx>( .basic() .ok_or_else(|| LlvmError::new("share clone returned void"))? } + Type::OwnedOpaque(kind) => { + let ptr_ty = context.ptr_type(Default::default()); + let name = match kind { + OpaqueKind::Interface => "mako_native_iface_clone", + OpaqueKind::HttpRequest => "mako_native_http_request_clone_ptr", + }; + let clone = external_function( + module, + name, + ptr_ty.fn_type(&[ptr_ty.into()], false), + ); + builder + .build_call(clone, &[loaded.into()], "clone.owned_opaque") + .map_err(builder_error)? + .try_as_basic_value() + .basic() + .ok_or_else(|| LlvmError::new("owned opaque clone returned void"))? + } Type::MapII | Type::MapSI | Type::MapSS @@ -1873,6 +1893,24 @@ fn llvm_emit_struct_drop<'ctx>( .build_call(drop, &[loaded.into()], "drop.share.call") .map_err(builder_error)?; } + Type::OwnedOpaque(kind) => { + let loaded = builder + .build_load(llvm_type(context, *field_ty), gep, "drop.owned_opaque") + .map_err(builder_error)?; + let ptr_ty = context.ptr_type(Default::default()); + let name = match kind { + OpaqueKind::Interface => "mako_native_iface_drop", + OpaqueKind::HttpRequest => "mako_native_http_request_drop_ptr", + }; + let drop = external_function( + module, + name, + context.void_type().fn_type(&[ptr_ty.into()], false), + ); + builder + .build_call(drop, &[loaded.into()], "drop.owned_opaque.call") + .map_err(builder_error)?; + } Type::MapII | Type::MapSI | Type::MapSS diff --git a/src/native_codegen.rs b/src/native_codegen.rs index 23ea3258..b87b5d54 100644 --- a/src/native_codegen.rs +++ b/src/native_codegen.rs @@ -1640,6 +1640,7 @@ fn ir_clif_type(ty: IrType) -> Result | IrType::Task | IrType::Opaque | IrType::Builder + | IrType::OwnedOpaque(_) | IrType::FnPtr | IrType::StructSlice(_) | IrType::ShareInt @@ -1719,6 +1720,23 @@ fn emit_struct_clone( let call = fb.ins().call(f, &[loaded]); fb.inst_results(call)[0] } + IrType::OwnedOpaque(kind) => { + let name = match kind { + native_ir::OpaqueKind::Interface => "mako_native_iface_clone", + native_ir::OpaqueKind::HttpRequest => { + "mako_native_http_request_clone_ptr" + } + }; + let mut sig = module.make_signature(); + sig.params.push(AbiParam::new(types::I64)); + sig.returns.push(AbiParam::new(types::I64)); + let id = module + .declare_function(name, Linkage::Import, &sig) + .map_err(|e| NativeError::new(e.to_string()))?; + let f = module.declare_func_in_func(id, &mut fb.func); + let call = fb.ins().call(f, &[loaded]); + fb.inst_results(call)[0] + } IrType::MapII | IrType::MapSI | IrType::MapSS @@ -1813,6 +1831,21 @@ fn emit_struct_drop( let f = module.declare_func_in_func(id, &mut fb.func); fb.ins().call(f, &[loaded]); } + IrType::OwnedOpaque(kind) => { + let name = match kind { + native_ir::OpaqueKind::Interface => "mako_native_iface_drop", + native_ir::OpaqueKind::HttpRequest => { + "mako_native_http_request_drop_ptr" + } + }; + let mut sig = module.make_signature(); + sig.params.push(AbiParam::new(types::I64)); + let id = module + .declare_function(name, Linkage::Import, &sig) + .map_err(|e| NativeError::new(e.to_string()))?; + let f = module.declare_func_in_func(id, &mut fb.func); + fb.ins().call(f, &[loaded]); + } IrType::MapII | IrType::MapSI | IrType::MapSS diff --git a/src/native_ir.rs b/src/native_ir.rs index b289c73c..ee3ac1ff 100644 --- a/src/native_ir.rs +++ b/src/native_ir.rs @@ -11,6 +11,12 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::fmt; +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum OpaqueKind { + Interface, + HttpRequest, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Type { I1, @@ -44,12 +50,14 @@ pub enum Type { /// and handles owned by a foreign library or one of our registries, and /// nothing at runtime tells them apart. A `SqlDB` is /// `(int64_t)(intptr_t)db.sqlite`, so freeing one corrupts the allocator. - /// A handle the runtime does own belongs in its own variant, like - /// `Builder` below, so it can carry a real destructor. + /// A handle the runtime does own belongs in its own variant so it can carry + /// a real destructor. Opaque, /// `StrBuilder` — a handle this runtime allocated (struct plus buffer) and /// therefore knows how to free, unlike the `Opaque` catch-all. Builder, + /// Runtime-owned opaque block with a compiler-selected clone/drop routine. + OwnedOpaque(OpaqueKind), /// `[]Struct` — owned pointer-array of heap struct pointers. StructSlice(u32), /// `map[int]int` — owned pointer to open-addressing table. @@ -187,6 +195,8 @@ pub enum MapValKind { ChanPOther, /// Other pointer-sized (Result, Option, Opaque, …) — exposed as I64. Other, + /// Runtime-owned opaque block stored in a pointer map or pointer slice. + OwnedOpaque(OpaqueKind), /// `map[Struct]int` / `map[Struct]string` / `map[Struct]float` — key is a /// heap struct (content equality); value is the scalar named here. StructKeyInt(u32), @@ -200,6 +210,8 @@ pub enum MapValKind { StructKeyStrSlice(u32), StructKeyFloatSlice(u32), StructKeyStructSlice(u32, u32), + /// `map[Struct]` with a runtime-owned opaque value. + StructKeyOwnedOpaque(u32, OpaqueKind), /// `map[Struct]V` for other pointer-sized values (chan, map, nested, …). /// Value is stored as i64; get retypes via expression context / from_type. StructKeyPtr(u32), @@ -327,6 +339,7 @@ impl MapValKind { MapValKind::Struct(id) => MapValKind::ChanPStruct(id), _ => MapValKind::ChanPOther, }, + Type::OwnedOpaque(kind) => MapValKind::OwnedOpaque(kind), _ => MapValKind::Other, } } @@ -410,6 +423,7 @@ impl MapValKind { MapValKind::ChanF => Type::ChanF, MapValKind::ChanPStruct(id) => Type::ChanP(MapValKind::Struct(id)), MapValKind::ChanPOther => Type::ChanP(MapValKind::Other), + MapValKind::OwnedOpaque(kind) => Type::OwnedOpaque(kind), MapValKind::Other => Type::I64, // NestedChan* already handled above; StructKey* below. // Struct-key maps: value type (key recovered separately in range). @@ -421,6 +435,7 @@ impl MapValKind { MapValKind::StructKeyStrSlice(_) => Type::StrSlice, MapValKind::StructKeyFloatSlice(_) => Type::FloatSlice, MapValKind::StructKeyStructSlice(_, eid) => Type::StructSlice(eid), + MapValKind::StructKeyOwnedOpaque(_, kind) => Type::OwnedOpaque(kind), // Pointer-sized payload; callers retype (chan/map/…) after get. MapValKind::StructKeyPtr(_) => Type::I64, } @@ -435,6 +450,7 @@ impl MapValKind { | MapValKind::StructKeyIntSlice(id) | MapValKind::StructKeyStrSlice(id) | MapValKind::StructKeyFloatSlice(id) + | MapValKind::StructKeyOwnedOpaque(id, _) | MapValKind::StructKeyPtr(id) => Some(id), MapValKind::StructKeyToStruct(kid, _) | MapValKind::StructKeyStructSlice(kid, _) => { Some(kid) @@ -473,6 +489,7 @@ impl Type { | Type::Task | Type::Opaque | Type::Builder + | Type::OwnedOpaque(_) | Type::StructSlice(_) | Type::ShareInt | Type::Struct(_) @@ -508,7 +525,7 @@ impl Type { | Type::FnPtr | Type::Opaque | Type::Builder - | Type::Builder + | Type::OwnedOpaque(_) | Type::ShareInt | Type::ChanI | Type::ChanS @@ -820,7 +837,7 @@ struct StructRegistry { variants: RefCell>>, /// Which layout ids are enums (as opposed to structs/tuples). enum_ids: RefCell>, - /// Interface type names — values are `Type::Opaque` fat-pointer handles. + /// Interface type names — values are owned fat-pointer boxes. interfaces: std::collections::HashSet, /// Interface → method names. interface_methods: HashMap>, @@ -1127,6 +1144,9 @@ fn scalar_type(ty: &TypeExpr) -> Result { // (immortal/static high-bit already covers literal views). TypeExpr::Named(name) if name == "string" || name == "string_view" => Ok(Type::Str), TypeExpr::Named(name) if name == "ShareInt" => Ok(Type::ShareInt), + TypeExpr::Named(name) if name == "HttpRequest" => { + Ok(Type::OwnedOpaque(OpaqueKind::HttpRequest)) + } TypeExpr::Array(inner) if matches!(inner.as_ref(), TypeExpr::Named(name) if name == "int" || name == "int64") => { Ok(Type::IntSlice) } @@ -1226,7 +1246,6 @@ fn scalar_type(ty: &TypeExpr) -> Result { | "Conn" | "UdpConn" | "WsConn" - | "HttpRequest" | "HttpParsed" | "HttpResponse" ) => @@ -1300,6 +1319,7 @@ fn is_field_type(ty: Type) -> bool { | Type::Task | Type::Opaque | Type::Builder + | Type::OwnedOpaque(_) | Type::FnPtr | Type::StructSlice(_) | Type::ShareInt @@ -1315,9 +1335,9 @@ fn resolve_type(ty: &TypeExpr, structs: &StructRegistry) -> Result Result MapValKind::StructKeyToStruct(kid, vid), + Type::OwnedOpaque(kind) => { + MapValKind::StructKeyOwnedOpaque(kid, kind) + } Type::Str => MapValKind::StructKeyStr(kid), Type::F64 => MapValKind::StructKeyFloat(kid), Type::I64 => MapValKind::StructKeyInt(kid), @@ -2933,6 +2956,7 @@ impl<'a> FunctionLowerer<'a> { | Type::Nursery | Type::Task | Type::Opaque + | Type::OwnedOpaque(_) | Type::ShareInt => { owned = self.take_bare_string_local(init, owned); // Borrowed heap sources must be cloned so the binding @@ -2985,11 +3009,15 @@ impl<'a> FunctionLowerer<'a> { } let ty = annotated.unwrap_or(inferred); let (value, owned) = if ty != inferred { - // Struct → interface (Opaque) conversion. - if ty == Type::Opaque && matches!(inferred, Type::Struct(_)) { - let boxed = self.box_iface(value, inferred)?; + // Struct → interface conversion. + if ty == Type::OwnedOpaque(OpaqueKind::Interface) + && matches!(inferred, Type::Struct(_)) + { + let boxed = self.box_iface(value, inferred, owned)?; (boxed, true) - } else if ty == Type::Opaque && inferred == Type::I64 { + } else if ty == Type::OwnedOpaque(OpaqueKind::Interface) + && inferred == Type::I64 + { // Unit interface from int placeholder (`let w: Writer = 0`). (value, false) } else if ty == Type::Str && inferred == Type::I64 { @@ -3427,6 +3455,7 @@ impl<'a> FunctionLowerer<'a> { | Type::Nursery | Type::Task | Type::Opaque + | Type::OwnedOpaque(_) | Type::StructSlice(_) | Type::ShareInt => { owned = self.take_bare_string_local(rhs, owned); @@ -3769,10 +3798,15 @@ impl<'a> FunctionLowerer<'a> { )); } let (nf, sm, nm, nfp, nsp) = self.struct_key_meta(sid); + let (vkind, vnf, vsm, vnm, vnfp, vnsp) = + self.struct_map_value_meta(vk); self.emit(Inst::Call { out: None, function: "mako_native_map_struct_key_set_ptr".into(), - args: vec![slice, idx, nf, sm, nm, nfp, nsp, v], + args: vec![ + slice, idx, nf, sm, nm, nfp, nsp, v, vkind, vnf, vsm, vnm, + vnfp, vnsp, + ], ret: None, }); if io { @@ -4945,6 +4979,7 @@ impl<'a> FunctionLowerer<'a> { | Type::Str | Type::ShareInt | Type::Opaque + | Type::OwnedOpaque(_) | Type::FnPtr | Type::Struct(_) ) && !out.iter().any(|(a, _)| a == &n) @@ -5413,6 +5448,7 @@ impl<'a> FunctionLowerer<'a> { | Some(Type::I1) | Some(Type::F64) | Some(Type::Opaque) + | Some(Type::OwnedOpaque(_)) | Some(Type::FnPtr) | Some(Type::Struct(_)) | Some(Type::Str) @@ -5455,6 +5491,7 @@ impl<'a> FunctionLowerer<'a> { | Type::StructSlice(_) | Type::PtrSlice(_) | Type::Opaque + | Type::OwnedOpaque(_) | Type::Struct(_) | Type::FnPtr | Type::Task @@ -6721,6 +6758,9 @@ impl<'a> FunctionLowerer<'a> { }); val = f; } + if elem_ty.is_heap() && !elem_ty.is_shared_handle() { + val = self.emit_clone(val, elem_ty); + } self.emit(Inst::Store { ptr: vslot, value: val, @@ -6751,10 +6791,20 @@ impl<'a> FunctionLowerer<'a> { } if vname != "_" { self.locals.insert(vname.clone(), (vslot, elem_ty)); - // Shared handles (channels) are borrows of map storage. if elem_ty.is_heap() && !elem_ty.is_shared_handle() { self.heap_owned.insert(vname.clone(), true); } + } else if elem_ty.is_heap() && !elem_ty.is_shared_handle() { + self.emit_drop(val, elem_ty); + let null = self.value(); + self.emit(Inst::NullHeap { + out: null, + ty: elem_ty, + }); + self.emit(Inst::Store { + ptr: vslot, + value: null, + }); } } ForIter::MapSI | ForIter::MapSPtr(_) => { @@ -6783,6 +6833,9 @@ impl<'a> FunctionLowerer<'a> { }); val = f; } + if elem_ty.is_heap() && !elem_ty.is_shared_handle() { + val = self.emit_clone(val, elem_ty); + } self.emit(Inst::Store { ptr: ks, value: key, @@ -6811,6 +6864,17 @@ impl<'a> FunctionLowerer<'a> { if elem_ty.is_heap() && !elem_ty.is_shared_handle() { self.heap_owned.insert(vname.clone(), true); } + } else if elem_ty.is_heap() && !elem_ty.is_shared_handle() { + self.emit_drop(val, elem_ty); + let null = self.value(); + self.emit(Inst::NullHeap { + out: null, + ty: elem_ty, + }); + self.emit(Inst::Store { + ptr: vslot, + value: null, + }); } } ForIter::MapSS => { @@ -7466,9 +7530,6 @@ impl<'a> FunctionLowerer<'a> { ret: Some(Type::I64), }); } - if owned { - self.emit_drop(slice, sty); - } let vty = vk.to_type(); if vty == Type::F64 { // Float stored as i64 bit pattern. @@ -7479,12 +7540,22 @@ impl<'a> FunctionLowerer<'a> { args: vec![out], ret: Some(Type::F64), }); + if owned { + self.emit_drop(slice, sty); + } return Ok((f, Type::F64, false)); } - let fixed = self.null_to_empty_heap(out, vty)?; - // Borrowed view into map storage (map still owns the value). - // Callers that need ownership must clone explicitly. - Ok((fixed, vty, false)) + let (fixed, value_owned) = + if vty.is_heap() && !vty.is_shared_handle() { + let clone = self.emit_clone(out, vty); + (self.null_to_empty_heap(clone, vty)?, true) + } else { + (out, false) + }; + if owned { + self.emit_drop(slice, sty); + } + Ok((fixed, vty, value_owned)) } Type::MapSI => { if ity != Type::Str { @@ -7521,9 +7592,6 @@ impl<'a> FunctionLowerer<'a> { if iowned { self.emit(Inst::DropString { value: idx }); } - if owned { - self.emit_drop(slice, sty); - } let vty = vk.to_type(); if vty == Type::F64 { let f = self.value(); @@ -7533,13 +7601,22 @@ impl<'a> FunctionLowerer<'a> { args: vec![out], ret: Some(Type::F64), }); + if owned { + self.emit_drop(slice, sty); + } return Ok((f, Type::F64, false)); } - // Missing key → empty slice/null instead of raw null pointer. - let fixed = self.null_to_empty_heap(out, vty)?; - // Borrowed view into map storage (map still owns the value). - // Callers that need ownership must clone explicitly. - Ok((fixed, vty, false)) + let (fixed, value_owned) = + if vty.is_heap() && !vty.is_shared_handle() { + let clone = self.emit_clone(out, vty); + (self.null_to_empty_heap(clone, vty)?, true) + } else { + (out, false) + }; + if owned { + self.emit_drop(slice, sty); + } + Ok((fixed, vty, value_owned)) } Type::MapSS => { if ity != Type::Str { @@ -8280,10 +8357,14 @@ impl<'a> FunctionLowerer<'a> { { let Type::Struct(sid) = kt else { unreachable!() }; let (nf, sm, nm, nfp, nsp) = self.struct_key_meta(sid); + let (vkind, vnf, vsm, vnm, vnfp, vnsp) = + self.struct_map_value_meta(vk); self.emit(Inst::Call { out: None, function: "mako_native_map_struct_key_delete_ptr".into(), - args: vec![m, k, nf, sm, nm, nfp, nsp], + args: vec![ + m, k, nf, sm, nm, nfp, nsp, vkind, vnf, vsm, vnm, vnfp, vnsp, + ], ret: None, }); if ko { @@ -8295,10 +8376,13 @@ impl<'a> FunctionLowerer<'a> { { let Type::Struct(sid) = kt else { unreachable!() }; let (nf, sm, nm, nfp, nsp) = self.struct_key_meta(sid); + let zero = self.const_int(0, Type::I64); self.emit(Inst::Call { out: None, function: "mako_native_map_struct_key_delete_ptr".into(), - args: vec![m, k, nf, sm, nm, nfp, nsp], + args: vec![ + m, k, nf, sm, nm, nfp, nsp, zero, zero, zero, zero, zero, zero, + ], ret: None, }); if ko { @@ -9707,15 +9791,22 @@ impl<'a> FunctionLowerer<'a> { if function == "maps_keys" && args.len() == 1 { let (m, mt, mo) = self.lower_expr(&args[0])?; let out = self.value(); - let (fname, ret_ty) = match mt { - Type::MapIPtr(vk) if vk.struct_key_id().is_some() => { - // Keys are owned struct pointers — return as []Struct. - let sid = vk.struct_key_id().unwrap(); - ( - "mako_native_maps_keys_ii_ptr", - Type::StructSlice(sid), - ) + if let Type::MapIPtr(vk) = mt { + if let Some(sid) = vk.struct_key_id() { + let (nf, sm, nm, nfp, nsp) = self.struct_key_meta(sid); + self.emit(Inst::Call { + out: Some(out), + function: "mako_native_maps_keys_struct_ptr".into(), + args: vec![m, nf, sm, nm, nfp, nsp], + ret: Some(Type::StructSlice(sid)), + }); + if mo { + self.emit_drop(m, mt); + } + return Ok((out, Type::StructSlice(sid), true)); } + } + let (fname, ret_ty) = match mt { Type::MapII | Type::MapIPtr(_) => { ("mako_native_maps_keys_ii_ptr", Type::IntSlice) } @@ -9745,6 +9836,42 @@ impl<'a> FunctionLowerer<'a> { if function == "maps_values" && args.len() == 1 { let (m, mt, mo) = self.lower_expr(&args[0])?; let out = self.value(); + if let Type::MapIPtr(vk) = mt { + let ret_ty = match vk { + MapValKind::StructKeyStr(_) => Some(Type::StrSlice), + MapValKind::StructKeyIntSlice(_) => { + Some(Type::PtrSlice(MapValKind::IntSlice)) + } + MapValKind::StructKeyStrSlice(_) => { + Some(Type::PtrSlice(MapValKind::StrSlice)) + } + MapValKind::StructKeyFloatSlice(_) => { + Some(Type::PtrSlice(MapValKind::FloatSlice)) + } + MapValKind::StructKeyToStruct(_, id) => Some(Type::StructSlice(id)), + MapValKind::StructKeyStructSlice(_, id) => { + Some(Type::PtrSlice(MapValKind::StructSlice(id))) + } + MapValKind::StructKeyOwnedOpaque(_, kind) => { + Some(Type::PtrSlice(MapValKind::OwnedOpaque(kind))) + } + _ => None, + }; + if let Some(ret_ty) = ret_ty { + let (vkind, vnf, vsm, vnm, vnfp, vnsp) = + self.struct_map_value_meta(vk); + self.emit(Inst::Call { + out: Some(out), + function: "mako_native_maps_values_struct_key".into(), + args: vec![m, vkind, vnf, vsm, vnm, vnfp, vnsp], + ret: Some(ret_ty), + }); + if mo { + self.emit_drop(m, mt); + } + return Ok((out, ret_ty, true)); + } + } let (fname, ret_ty) = match mt { Type::MapII => ("mako_native_maps_values_ii_ptr", Type::IntSlice), Type::MapIPtr(MapValKind::StructKeyInt(_)) => { @@ -9840,10 +9967,14 @@ impl<'a> FunctionLowerer<'a> { if let Type::MapIPtr(vk) = mt { if let Some(sid) = vk.struct_key_id() { let (nf, sm, nm, nfp, nsp) = self.struct_key_meta(sid); + let (vkind, vnf, vsm, vnm, vnfp, vnsp) = + self.struct_map_value_meta(vk); self.emit(Inst::Call { out: Some(out), function: "mako_native_map_struct_key_clone_ptr".into(), - args: vec![m, nf, sm, nm, nfp, nsp], + args: vec![ + m, nf, sm, nm, nfp, nsp, vkind, vnf, vsm, vnm, vnfp, vnsp, + ], ret: Some(mt), }); if mo { @@ -9988,6 +10119,22 @@ impl<'a> FunctionLowerer<'a> { "native IR: maps_clear requires a borrowed map local", )); } + if let Type::MapIPtr(vk) = mt { + if let Some(sid) = vk.struct_key_id() { + let (nf, sm, nm, nfp, nsp) = self.struct_key_meta(sid); + let (vkind, vnf, vsm, vnm, vnfp, vnsp) = + self.struct_map_value_meta(vk); + self.emit(Inst::Call { + out: None, + function: "mako_native_maps_clear_struct_key".into(), + args: vec![ + m, nf, sm, nm, nfp, nsp, vkind, vnf, vsm, vnm, vnfp, vnsp, + ], + ret: None, + }); + return Ok((self.const_int(0, Type::I64), Type::I64, false)); + } + } let fname = match mt { Type::MapII | Type::MapIPtr(_) => "mako_native_maps_clear_ii", Type::MapSI | Type::MapSPtr(_) => "mako_native_maps_clear_si", @@ -10021,10 +10168,15 @@ impl<'a> FunctionLowerer<'a> { if let Type::MapIPtr(vk) = dt { if let Some(sid) = vk.struct_key_id() { let (nf, sm, nm, nfp, nsp) = self.struct_key_meta(sid); + let (vkind, vnf, vsm, vnm, vnfp, vnsp) = + self.struct_map_value_meta(vk); self.emit(Inst::Call { out: None, function: "mako_native_maps_copy_struct_key".into(), - args: vec![dst, src, nf, sm, nm, nfp, nsp], + args: vec![ + dst, src, nf, sm, nm, nfp, nsp, vkind, vnf, vsm, vnm, vnfp, + vnsp, + ], ret: None, }); if s_owned { @@ -10153,9 +10305,9 @@ impl<'a> FunctionLowerer<'a> { (expected, actual), (Type::I64, Type::I1) | (Type::I1, Type::I64) - | (Type::Opaque, Type::Struct(_)) - | (Type::Opaque, Type::I64) - | (Type::I64, Type::Opaque) + | (Type::OwnedOpaque(OpaqueKind::Interface), Type::Struct(_)) + | (Type::OwnedOpaque(OpaqueKind::Interface), Type::I64) + | (Type::I64, Type::OwnedOpaque(OpaqueKind::Interface)) ) }; let (fn_name, params, ret) = mono_candidates @@ -10190,7 +10342,7 @@ impl<'a> FunctionLowerer<'a> { } let mut lowered = Vec::with_capacity(args.len()); let mut temporary_owned = Vec::new(); - for (arg_expr, ((value0, actual0, owned), expected)) in args + for (arg_expr, ((value0, actual0, mut owned), expected)) in args .iter() .zip(lowered_raw.into_iter().zip(params)) { @@ -10218,14 +10370,21 @@ impl<'a> FunctionLowerer<'a> { value = casted; actual = Type::I1; } else if actual != expected { - // Interface param: box concrete struct into Opaque. - if expected == Type::Opaque && matches!(actual, Type::Struct(_)) { - value = self.box_iface(value, actual)?; - actual = Type::Opaque; - } else if expected == Type::Opaque && actual == Type::I64 { + // Interface param: box the concrete struct. + if expected == Type::OwnedOpaque(OpaqueKind::Interface) + && matches!(actual, Type::Struct(_)) + { + value = self.box_iface(value, actual, owned)?; + actual = Type::OwnedOpaque(OpaqueKind::Interface); + owned = true; + } else if expected == Type::OwnedOpaque(OpaqueKind::Interface) + && actual == Type::I64 + { // Unit iface from int placeholder. - actual = Type::Opaque; - } else if expected == Type::I64 && actual == Type::Opaque { + actual = Type::OwnedOpaque(OpaqueKind::Interface); + } else if expected == Type::I64 + && actual == Type::OwnedOpaque(OpaqueKind::Interface) + { // Opaque handle passed where i64 fd/handle expected. actual = Type::I64; } else { @@ -13763,8 +13922,8 @@ impl<'a> FunctionLowerer<'a> { } } - // Dyn / unit interface method on Opaque (or bare int placeholder). - if rty == Type::Opaque || rty == Type::I64 { + // Dyn / unit interface method on a boxed interface (or bare int placeholder). + if rty == Type::OwnedOpaque(OpaqueKind::Interface) || rty == Type::I64 { if let Some(result) = self.lower_iface_method(recv, rty, rowned, method, args)? { return Ok(result); } @@ -14087,6 +14246,8 @@ impl<'a> FunctionLowerer<'a> { | (Type::ChanP(_), Type::ChanF) | (Type::Opaque, Type::I64) | (Type::I64, Type::Opaque) + | (Type::OwnedOpaque(_), Type::I64) + | (Type::I64, Type::OwnedOpaque(_)) ); if !compatible { return Err(IrError::new( @@ -20430,24 +20591,24 @@ impl<'a> FunctionLowerer<'a> { "http_request_parse" if args.len() == 1 => Some(( "mako_native_http_request_parse_ptr", &[Type::Str], - Some(Type::Opaque), - false, + Some(Type::OwnedOpaque(OpaqueKind::HttpRequest)), + true, )), "http_request_method" if args.len() == 1 => Some(( "mako_native_http_request_method_ptr", - &[Type::Opaque], + &[Type::OwnedOpaque(OpaqueKind::HttpRequest)], Some(Type::Str), true, )), "http_request_path" if args.len() == 1 => Some(( "mako_native_http_request_path_ptr", - &[Type::Opaque], + &[Type::OwnedOpaque(OpaqueKind::HttpRequest)], Some(Type::Str), true, )), "http_request_body" if args.len() == 1 => Some(( "mako_native_http_request_body_ptr", - &[Type::Opaque], + &[Type::OwnedOpaque(OpaqueKind::HttpRequest)], Some(Type::Str), true, )), @@ -25571,7 +25732,11 @@ impl<'a> FunctionLowerer<'a> { "http_route_match" if args.len() == 3 => Some(( "mako_native_http_route_match_ptr", - &[Type::Opaque, Type::Str, Type::Str], + &[ + Type::OwnedOpaque(OpaqueKind::HttpRequest), + Type::Str, + Type::Str, + ], Some(Type::I64), false, )), @@ -25595,7 +25760,11 @@ impl<'a> FunctionLowerer<'a> { )), "http_route_param" if args.len() == 3 => Some(( "mako_native_http_route_param_ptr", - &[Type::Opaque, Type::Str, Type::Str], + &[ + Type::OwnedOpaque(OpaqueKind::HttpRequest), + Type::Str, + Type::Str, + ], Some(Type::Str), true, )), @@ -25613,7 +25782,7 @@ impl<'a> FunctionLowerer<'a> { )), "router_match" if args.len() == 2 => Some(( "mako_native_router_match_ptr", - &[Type::I64, Type::Opaque], + &[Type::I64, Type::OwnedOpaque(OpaqueKind::HttpRequest)], Some(Type::Str), true, )), @@ -26749,13 +26918,20 @@ impl<'a> FunctionLowerer<'a> { )), "middleware_allow_methods" if args.len() == 2 => Some(( "mako_native_middleware_allow_methods_ptr", - &[Type::Opaque, Type::Str], + &[ + Type::OwnedOpaque(OpaqueKind::HttpRequest), + Type::Str, + ], Some(Type::I64), false, )), "router_param" if args.len() == 3 => Some(( "mako_native_router_param_ptr", - &[Type::I64, Type::Opaque, Type::Str], + &[ + Type::I64, + Type::OwnedOpaque(OpaqueKind::HttpRequest), + Type::Str, + ], Some(Type::Str), true, )), @@ -29191,11 +29367,14 @@ impl<'a> FunctionLowerer<'a> { Ok(Some(result)) } - /// Box a concrete struct value into an opaque interface handle `{tag, data}`. - fn box_iface(&mut self, value: Value, ty: Type) -> Result { + /// Box a concrete struct value into an owned interface handle `{tag, data}`. + fn box_iface(&mut self, mut value: Value, ty: Type, owned: bool) -> Result { let Type::Struct(id) = ty else { return Err(IrError::new("native IR: box_iface expects struct")); }; + if !owned { + value = self.emit_clone(value, ty); + } let name = self.structs.layout_name(id); let tag = self .structs @@ -29204,13 +29383,13 @@ impl<'a> FunctionLowerer<'a> { .copied() .unwrap_or(0); let tag_v = self.const_int(tag, Type::I64); - // Ensure we pass an owned pointer into the box (clone if needed is on caller). + let (nf, sm, nm, nfp, nsp) = self.struct_key_meta(id); let out = self.value(); self.emit(Inst::Call { out: Some(out), function: "mako_native_iface_box".into(), - args: vec![tag_v, value], - ret: Some(Type::Opaque), + args: vec![tag_v, value, nf, sm, nm, nfp, nsp], + ret: Some(Type::OwnedOpaque(OpaqueKind::Interface)), }); Ok(out) } @@ -29476,14 +29655,39 @@ impl<'a> FunctionLowerer<'a> { ret: None, }); } - Type::StructSlice(_) => self.emit(Inst::Call { + Type::OwnedOpaque(kind) => self.emit(Inst::Call { out: None, - // Free each calloc'd struct block, then the pointer array. - function: "mako_native_ptr_slice_drop_free_elems".into(), + function: match kind { + OpaqueKind::Interface => "mako_native_iface_drop", + OpaqueKind::HttpRequest => "mako_native_http_request_drop_ptr", + } + .into(), args: vec![value], ret: None, }), + Type::StructSlice(id) => { + let (nf, sm, nm, nfp, nsp) = self.struct_key_meta(id); + self.emit(Inst::Call { + out: None, + function: "mako_native_struct_slice_drop_ptr".into(), + args: vec![value, nf, sm, nm, nfp, nsp], + ret: None, + }); + } Type::StrSlice => self.emit(Inst::DropStrSlice { value }), + Type::MapIPtr(vk) if vk.struct_key_id().is_some() => { + let sid = vk.struct_key_id().unwrap(); + let (nf, sm, nm, nfp, nsp) = self.struct_key_meta(sid); + let (vkind, vnf, vsm, vnm, vnfp, vnsp) = self.struct_map_value_meta(vk); + self.emit(Inst::Call { + out: None, + function: "mako_native_map_struct_key_drop_ptr".into(), + args: vec![ + value, nf, sm, nm, nfp, nsp, vkind, vnf, vsm, vnm, vnfp, vnsp, + ], + ret: None, + }); + } Type::MapII | Type::MapIPtr(_) => self.emit(Inst::Call { out: None, function: "mako_native_map_ii_drop_ptr".into(), @@ -29497,24 +29701,32 @@ impl<'a> FunctionLowerer<'a> { ret: None, }), Type::PtrSlice(vk) => { - // []Struct via PtrSlice still free's each calloc block. - // Everything else is shell-only: channels/opaque are shared - // handles (must not free), and nested slice/map headers need - // typed element drops that the C free() loop cannot do. - // Nested containers may leak element data until a full deep - // walker lands; correctness > perfect reclaim for now. - let free_elems = matches!(vk.to_type(), Type::Struct(_)); - self.emit(Inst::Call { - out: None, - function: if free_elems { - "mako_native_ptr_slice_drop_free_elems".into() - } else { - "mako_native_ptr_slice_drop".into() - }, - args: vec![value], - ret: None, - }); - }, + if matches!( + vk, + MapValKind::IntSlice + | MapValKind::StrSlice + | MapValKind::FloatSlice + | MapValKind::Struct(_) + | MapValKind::StructSlice(_) + | MapValKind::OwnedOpaque(_) + ) { + let (vkind, vnf, vsm, vnm, vnfp, vnsp) = + self.struct_map_value_meta(vk); + self.emit(Inst::Call { + out: None, + function: "mako_native_ptr_slice_drop_typed".into(), + args: vec![value, vkind, vnf, vsm, vnm, vnfp, vnsp], + ret: None, + }); + } else { + self.emit(Inst::Call { + out: None, + function: "mako_native_ptr_slice_drop".into(), + args: vec![value], + ret: None, + }); + } + } Type::MapSS => self.emit(Inst::Call { out: None, function: "mako_native_map_ss_drop_ptr".into(), @@ -30043,6 +30255,38 @@ impl<'a> FunctionLowerer<'a> { ) } + /// Runtime drop/clone metadata for values owned by a content-keyed map. + fn struct_map_value_meta( + &mut self, + kind: MapValKind, + ) -> (Value, Value, Value, Value, Value, Value) { + let (drop_kind, struct_id) = match kind { + MapValKind::OwnedStr | MapValKind::StructKeyStr(_) => (1, None), + MapValKind::Struct(id) | MapValKind::StructKeyToStruct(_, id) => { + (2, Some(id)) + } + MapValKind::IntSlice | MapValKind::StructKeyIntSlice(_) => (3, None), + MapValKind::StrSlice | MapValKind::StructKeyStrSlice(_) => (4, None), + MapValKind::FloatSlice | MapValKind::StructKeyFloatSlice(_) => (5, None), + MapValKind::StructSlice(id) | MapValKind::StructKeyStructSlice(_, id) => { + (6, Some(id)) + } + MapValKind::OwnedOpaque(OpaqueKind::Interface) + | MapValKind::StructKeyOwnedOpaque(_, OpaqueKind::Interface) => (7, None), + MapValKind::OwnedOpaque(OpaqueKind::HttpRequest) + | MapValKind::StructKeyOwnedOpaque(_, OpaqueKind::HttpRequest) => (8, None), + _ => (0, None), + }; + let kind = self.const_int(drop_kind, Type::I64); + if let Some(id) = struct_id { + let (nf, sm, nm, nfp, nsp) = self.struct_key_meta(id); + (kind, nf, sm, nm, nfp, nsp) + } else { + let zero = self.const_int(0, Type::I64); + (kind, zero, zero, zero, zero, zero) + } + } + fn map_i_key(&mut self, key: Value, kt: Type, ko: bool) -> Result { match kt { Type::I1 => { @@ -30698,15 +30942,24 @@ impl<'a> FunctionLowerer<'a> { // Handles are not deep-cloned; share the pointer (caller owns). return value; } + Type::OwnedOpaque(OpaqueKind::HttpRequest) => self.emit(Inst::Call { + out: Some(out), + function: "mako_native_http_request_clone_ptr".into(), + args: vec![value], + ret: Some(ty), + }), + Type::OwnedOpaque(OpaqueKind::Interface) => self.emit(Inst::Call { + out: Some(out), + function: "mako_native_iface_clone".into(), + args: vec![value], + ret: Some(ty), + }), Type::StructSlice(sid) => { - // Deep clone: shallow header clone would share element pointers - // that DropStruct frees — Result[Ok([]Point)] was UAF. - let nbytes = (self.structs.field_count(sid) * 8) as i64; - let nb = self.const_int(nbytes, Type::I64); + let (nf, sm, nm, nfp, nsp) = self.struct_key_meta(sid); self.emit(Inst::Call { out: Some(out), - function: "mako_native_ptr_slice_clone_deep".into(), - args: vec![value, nb], + function: "mako_native_struct_slice_clone_ptr".into(), + args: vec![value, nf, sm, nm, nfp, nsp], ret: Some(ty), }); } @@ -30723,6 +30976,26 @@ impl<'a> FunctionLowerer<'a> { args: vec![value], ret: Some(ty), }), + Type::PtrSlice(vk) + if matches!( + vk, + MapValKind::IntSlice + | MapValKind::StrSlice + | MapValKind::FloatSlice + | MapValKind::Struct(_) + | MapValKind::StructSlice(_) + | MapValKind::OwnedOpaque(_) + ) => + { + let (vkind, vnf, vsm, vnm, vnfp, vnsp) = + self.struct_map_value_meta(vk); + self.emit(Inst::Call { + out: Some(out), + function: "mako_native_ptr_slice_clone_typed".into(), + args: vec![value, vkind, vnf, vsm, vnm, vnfp, vnsp], + ret: Some(ty), + }); + } Type::PtrSlice(_) => self.emit(Inst::Call { out: Some(out), function: "mako_native_ptr_slice_clone".into(), @@ -31274,6 +31547,45 @@ mod tests { .flat_map(|b| &b.instructions) .any(|i| matches!(i, Inst::StringLen { .. }))); } + + #[test] + fn emits_typed_drops_for_owned_handles_and_struct_key_maps() { + let source = r#" + struct Counter { n: int } + interface Adder { fn add(int) -> int } + fn Adder_add(self: Counter, delta: int) -> int { + return self.n + delta + } + struct Label { text: string, id: int } + fn main() { + let counter = Counter { n: 2 } + let adder: Adder = counter + let adder_copy = adder + let request = http_request_parse("GET / HTTP/1.1\r\n\r\n") + let request_copy = request + let mut values = make(map[Label]int) + values[Label { text: "a", id: 1 }] = 1 + print_int(adder_copy.add(len(http_request_method(request_copy))) + len(values)) + } + "#; + let tokens = Lexer::new(source).tokenize().unwrap(); + let program = Parser::new(tokens).parse().unwrap(); + let module = lower(&program).unwrap(); + let calls = module + .functions + .iter() + .flat_map(|function| &function.blocks) + .flat_map(|block| &block.instructions) + .filter_map(|instruction| match instruction { + Inst::Call { function, .. } => Some(function.as_str()), + _ => None, + }) + .collect::>(); + + assert!(calls.contains(&"mako_native_iface_drop")); + assert!(calls.contains(&"mako_native_http_request_drop_ptr")); + assert!(calls.contains(&"mako_native_map_struct_key_drop_ptr")); + } } From 5c29494c7b8622d1721db1d09c14242da9f4d969 Mon Sep 17 00:00:00 2001 From: "Attah N." Date: Sun, 26 Jul 2026 16:22:02 +0100 Subject: [PATCH 2/4] Fix recursive native handle drops --- CHANGELOG.md | 8 +- docs/MEMORY_SAFETY.md | 2 +- .../native_owned_handle_drop_test.mko | 57 +++- runtime/native_bridge.c | 29 +- runtime/native_runtime.c | 1 + scripts/memory-safety-gate.sh | 23 +- src/llvm_codegen.rs | 12 +- src/native_codegen.rs | 14 +- src/native_ir.rs | 251 ++++++++++++++++-- 9 files changed, 312 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd820f35..865d498f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -119,9 +119,11 @@ share buffers, so freeing both input and output would be a double free; values from foreign handles. Interface boxes are reference-counted, `http_request_parse` results are deep-cloned and dropped, and foreign database, registry, and OS handles remain on their explicit close paths. Content-keyed -struct maps also clone and release their owned keys and values on read, -overwrite, delete, copy, clear, and final drop. The focused native fixture and -the full struct-key map suite pass under LeakSanitizer and AddressSanitizer. +struct maps also clone values returned by reads and release stored keys and +values on overwrite, delete, copy, clear, and final drop. Interface boxes retain +the concrete type's compiler-generated recursive destructor, including slices, +maps, nested structs, and owned handles. The focused native fixture and the full +struct-key map suite pass under LeakSanitizer and AddressSanitizer. ### General networking primitives (protocol-agnostic) diff --git a/docs/MEMORY_SAFETY.md b/docs/MEMORY_SAFETY.md index 9ffa8354..423abe49 100644 --- a/docs/MEMORY_SAFETY.md +++ b/docs/MEMORY_SAFETY.md @@ -65,7 +65,7 @@ There is **no** “let the GC clean it up later.” | **Gate script** | `./scripts/memory-safety-gate.sh` | ```bash -# Ownership + leak tests on C and native; ASan when the toolchain supports it: +# Ownership tests on C and native; native ASan/LSan when the toolchain supports it: ./scripts/memory-safety-gate.sh MAKO_MS_SKIP_HTTP=1 ./scripts/memory-safety-gate.sh # skip the slow HTTP soak ``` diff --git a/examples/native/owned_handle_drop/native_owned_handle_drop_test.mko b/examples/native/owned_handle_drop/native_owned_handle_drop_test.mko index cdc6e7d6..ad69b4cb 100644 --- a/examples/native/owned_handle_drop/native_owned_handle_drop_test.mko +++ b/examples/native/owned_handle_drop/native_owned_handle_drop_test.mko @@ -11,6 +11,19 @@ struct AdderHolder { value: Adder } +struct Bag { + items: []int + tag: string +} + +interface Summer { + fn sum(int) -> int +} + +struct SummerHolder { + value: Summer +} + struct RequestHolder { value: HttpRequest } @@ -19,6 +32,10 @@ fn Adder_add(self: Counter, delta: int) -> int { return self.n + delta } +fn Summer_sum(self: Bag, delta: int) -> int { + return self.items[0] + self.items[1] + len(self.tag) + delta +} + struct Label { text: string id: int @@ -29,15 +46,35 @@ fn interface_once(n: int) -> int { let adder: Adder = counter let first = AdderHolder { value: adder } let second = first - return first.value.add(1) + second.value.add(1) + let result = first.value.add(1) + second.value.add(1) + assert_eq(result, n * 2 + 2) + return result +} + +fn nonflat_interface_once(n: int) -> int { + let bag = Bag { + items: [n, n + 1], + tag: int_to_string(n), + } + let summer: Summer = bag + let first = SummerHolder { value: summer } + let second = first + let third = second + let expected = n * 2 + 2 + len(int_to_string(n)) + assert_eq(first.value.sum(1), expected) + assert_eq(second.value.sum(1), expected) + assert_eq(third.value.sum(1), expected) + return expected * 3 } fn request_once() -> int { - let raw = "POST /items HTTP/1.1\r\nHost: example.test\r\n\r\nbody" - let request = http_request_parse(raw) + var request = http_request_parse("GET /old HTTP/1.1\r\n\r\n") + request = http_request_parse("POST /items HTTP/1.1\r\nHost: example.test\r\n\r\nbody") let first = RequestHolder { value: request } let second = first - return len(http_request_method(first.value)) + len(http_request_path(second.value)) + let result = len(http_request_method(first.value)) + len(http_request_path(second.value)) + assert_eq(result, 10) + return result } fn struct_key_map_once(n: int) -> int { @@ -47,8 +84,14 @@ fn struct_key_map_once(n: int) -> int { text: int_to_string(n + 1), id: n + 1, } + values[Label { text: text, id: n }] = Label { + text: int_to_string(n + 2), + id: n + 2, + } let copy = maps_clone(values) - return copy[Label { text: text, id: n }].id + let result = copy[Label { text: text, id: n }].id + assert_eq(result, n + 2) + return result } fn TestOwnedNativeHandlesDrop() { @@ -56,9 +99,11 @@ fn TestOwnedNativeHandlesDrop() { let mut total = 0 while i < 200 { total = total + interface_once(i) + total = total + nonflat_interface_once(i) total = total + request_once() total = total + struct_key_map_once(i) i = i + 1 } - assert(total > 0) + assert_eq(i, 200) + assert_eq(total, 184570) } diff --git a/runtime/native_bridge.c b/runtime/native_bridge.c index 1a80102c..39407053 100644 --- a/runtime/native_bridge.c +++ b/runtime/native_bridge.c @@ -1714,36 +1714,22 @@ int64_t mako_native_http2_conn_new(void) { return c ? (int64_t)(intptr_t)c : -1; } -void mako_native_struct_drop_typed( - void *value, int64_t nfields, int64_t str_mask, - int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack -); +typedef void (*MakoNativeIfaceDrop)(void *); -/* Interface fat pointer with the concrete value's drop metadata. */ +/* Interface fat pointer with the concrete value's compiler-generated drop. */ typedef struct { int64_t tag; void *data; - int64_t nfields; - int64_t str_mask; - int64_t nest_mask; - int64_t nest_nf_pack; - int64_t nest_sm_pack; + MakoNativeIfaceDrop drop; _Atomic size_t refs; } MakoNativeIface; -void *mako_native_iface_box( - int64_t tag, void *data, int64_t nfields, int64_t str_mask, - int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack -) { +void *mako_native_iface_box(int64_t tag, void *data, MakoNativeIfaceDrop drop) { MakoNativeIface *b = (MakoNativeIface *)malloc(sizeof(MakoNativeIface)); if (!b) abort(); b->tag = tag; b->data = data; /* takes ownership of struct heap block */ - b->nfields = nfields; - b->str_mask = str_mask; - b->nest_mask = nest_mask; - b->nest_nf_pack = nest_nf_pack; - b->nest_sm_pack = nest_sm_pack; + b->drop = drop; atomic_init(&b->refs, 1); return b; } @@ -1766,10 +1752,7 @@ void mako_native_iface_drop(void *box) { MakoNativeIface *b = (MakoNativeIface *)box; if (!b) return; if (atomic_fetch_sub_explicit(&b->refs, 1, memory_order_acq_rel) != 1) return; - mako_native_struct_drop_typed( - b->data, b->nfields, b->str_mask, - b->nest_mask, b->nest_nf_pack, b->nest_sm_pack - ); + if (b->drop) b->drop(b->data); free(box); } diff --git a/runtime/native_runtime.c b/runtime/native_runtime.c index fe6cbcd4..220f03b7 100644 --- a/runtime/native_runtime.c +++ b/runtime/native_runtime.c @@ -1568,6 +1568,7 @@ static void *mako_native_struct_key_clone( int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack ) { if (!key || nfields <= 0) return NULL; + if (nfields > 62) nfields = 62; size_t bytes = (size_t)nfields * sizeof(int64_t); int64_t *out = (int64_t *)malloc(bytes); if (!out) abort(); diff --git a/scripts/memory-safety-gate.sh b/scripts/memory-safety-gate.sh index 9bca081f..a9e36408 100755 --- a/scripts/memory-safety-gate.sh +++ b/scripts/memory-safety-gate.sh @@ -90,8 +90,8 @@ fi # allocations made from generated code too. A bad access *inside* Cranelift # output is still not visible — that is what the C backend's ASan run is for. # -# Soft-fails like the ASan step below: not every host has a sanitizer runtime, -# and this gate must stay runnable on developer machines. +# Linux sanitizer failures are hard failures. Other hosts skip this block +# because the native sanitizer runtime is not reliable there. echo "=== memory-safety-gate: native backend under LeakSanitizer ===" if [[ "$(uname -s)" == "Linux" ]] && "$mako_bin" build --help 2>/dev/null | grep -q 'native'; then ms_native_leak_failed=0 @@ -116,6 +116,25 @@ else echo "memory-safety-gate: native LSan skipped (Linux-only; Mac sanitizer runtime hangs)" fi +echo "=== memory-safety-gate: native backend under AddressSanitizer ===" +if [[ "$(uname -s)" == "Linux" ]] && "$mako_bin" build --help 2>/dev/null | grep -q 'native'; then + ms_native_asan_failed=0 + for f in examples/native/owned_handle_drop/native_owned_handle_drop_test.mko \ + examples/testing/map_struct_key_test.mko; do + if ! "$mako_bin" test "$repo_dir/$f" --backend native --sanitize address \ + >/tmp/mako-ms-native-asan.out 2>&1; then + echo "memory-safety-gate: native ASan FAILED on $f" >&2 + tail -20 /tmp/mako-ms-native-asan.out >&2 || true + ms_native_asan_failed=1 + else + echo " native+asan ok $f" + fi + done + [[ "$ms_native_asan_failed" -eq 0 ]] || exit 1 +else + echo "memory-safety-gate: native ASan skipped (Linux-only)" +fi + echo "=== memory-safety-gate: ASan (optional if toolchain supports) ===" set +e "$mako_bin" test "$repo_dir/examples/testing/memory_safety_contract_test.mko" \ diff --git a/src/llvm_codegen.rs b/src/llvm_codegen.rs index 2c424223..a7f46a48 100644 --- a/src/llvm_codegen.rs +++ b/src/llvm_codegen.rs @@ -1,7 +1,7 @@ //! Optimizing LLVM emitter for backend-neutral native IR. use crate::ast::{BinOp, UnaryOp}; -use crate::native_ir::{self, Inst, OpaqueKind, Terminator, Type, Value}; +use crate::native_ir::{self, Inst, Terminator, Type, Value}; use inkwell::basic_block::BasicBlock; use inkwell::builder::Builder; use inkwell::context::Context; @@ -1722,10 +1722,7 @@ fn llvm_emit_struct_clone<'ctx>( } Type::OwnedOpaque(kind) => { let ptr_ty = context.ptr_type(Default::default()); - let name = match kind { - OpaqueKind::Interface => "mako_native_iface_clone", - OpaqueKind::HttpRequest => "mako_native_http_request_clone_ptr", - }; + let name = kind.clone_fn(); let clone = external_function( module, name, @@ -1898,10 +1895,7 @@ fn llvm_emit_struct_drop<'ctx>( .build_load(llvm_type(context, *field_ty), gep, "drop.owned_opaque") .map_err(builder_error)?; let ptr_ty = context.ptr_type(Default::default()); - let name = match kind { - OpaqueKind::Interface => "mako_native_iface_drop", - OpaqueKind::HttpRequest => "mako_native_http_request_drop_ptr", - }; + let name = kind.drop_fn(); let drop = external_function( module, name, diff --git a/src/native_codegen.rs b/src/native_codegen.rs index b87b5d54..1340273a 100644 --- a/src/native_codegen.rs +++ b/src/native_codegen.rs @@ -1721,12 +1721,7 @@ fn emit_struct_clone( fb.inst_results(call)[0] } IrType::OwnedOpaque(kind) => { - let name = match kind { - native_ir::OpaqueKind::Interface => "mako_native_iface_clone", - native_ir::OpaqueKind::HttpRequest => { - "mako_native_http_request_clone_ptr" - } - }; + let name = kind.clone_fn(); let mut sig = module.make_signature(); sig.params.push(AbiParam::new(types::I64)); sig.returns.push(AbiParam::new(types::I64)); @@ -1832,12 +1827,7 @@ fn emit_struct_drop( fb.ins().call(f, &[loaded]); } IrType::OwnedOpaque(kind) => { - let name = match kind { - native_ir::OpaqueKind::Interface => "mako_native_iface_drop", - native_ir::OpaqueKind::HttpRequest => { - "mako_native_http_request_drop_ptr" - } - }; + let name = kind.drop_fn(); let mut sig = module.make_signature(); sig.params.push(AbiParam::new(types::I64)); let id = module diff --git a/src/native_ir.rs b/src/native_ir.rs index ee3ac1ff..a63dda1a 100644 --- a/src/native_ir.rs +++ b/src/native_ir.rs @@ -17,6 +17,22 @@ pub enum OpaqueKind { HttpRequest, } +impl OpaqueKind { + pub(crate) fn clone_fn(self) -> &'static str { + match self { + Self::Interface => "mako_native_iface_clone", + Self::HttpRequest => "mako_native_http_request_clone_ptr", + } + } + + pub(crate) fn drop_fn(self) -> &'static str { + match self { + Self::Interface => "mako_native_iface_drop", + Self::HttpRequest => "mako_native_http_request_drop_ptr", + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Type { I1, @@ -821,6 +837,28 @@ pub struct Module { pub structs: Vec, } +fn struct_drop_helper_name(id: u32) -> String { + format!("__mako.drop_struct.{id}") +} + +fn struct_drop_helper(id: u32) -> Function { + let value = Value(0); + Function { + name: struct_drop_helper_name(id), + params: vec![("value".into(), value, Type::Struct(id))], + ret: None, + blocks: vec![BasicBlock { + instructions: vec![Inst::DropStruct { + value, + struct_id: id, + }], + terminator: Some(Terminator::Return(None)), + }], + entry: BlockId(0), + next_value: 1, + } +} + /// Registry of aggregate layouts. Named structs are resolved up front; /// anonymous tuple shapes are interned on demand during lowering (hence the /// interior mutability). Both share one `id` space and one `StructLayout` list, @@ -2496,6 +2534,21 @@ pub fn lower_with_tests(program: &Program, test_fns: &[String]) -> Result function + .strip_prefix("__mako.drop_struct.") + .and_then(|id| id.parse::().ok()), + _ => None, + }) + .collect::>(); + drop_helpers.sort_unstable(); + drop_helpers.dedup(); + functions.extend(drop_helpers.into_iter().map(struct_drop_helper)); // Dedupe by name (monomorph + kick edge cases). { let mut seen = std::collections::HashSet::new(); @@ -2507,6 +2560,46 @@ pub fn lower_with_tests(program: &Program, test_fns: &[String]) -> Result Result<(), IrError> { + let layouts = structs.layouts.borrow(); + for layout in layouts.iter() { + if layout.fields.len() > 62 { + return Err(IrError::new(format!( + "native IR: struct `{}` has {} fields; native struct metadata supports at most 62", + layout.name, + layout.fields.len() + ))); + } + + let nested = layout + .fields + .iter() + .filter_map(|(_, ty)| match ty { + Type::Struct(id) => Some(*id), + _ => None, + }) + .collect::>(); + if nested.len() > 4 { + return Err(IrError::new(format!( + "native IR: struct `{}` has {} nested struct fields; native struct metadata supports at most 4", + layout.name, + nested.len() + ))); + } + for id in nested { + let nested_layout = &layouts[id as usize]; + if nested_layout.fields.len() > 16 { + return Err(IrError::new(format!( + "native IR: nested struct `{}` has {} fields; native struct metadata supports at most 16", + nested_layout.name, + nested_layout.fields.len() + ))); + } + } + } + Ok(()) +} + /// Fold `const NAME = …` / `const fn` initializers into int and string tables. fn collect_consts(program: &Program) -> (HashMap, HashMap) { let mut const_fns: HashMap = HashMap::new(); @@ -29383,12 +29476,16 @@ impl<'a> FunctionLowerer<'a> { .copied() .unwrap_or(0); let tag_v = self.const_int(tag, Type::I64); - let (nf, sm, nm, nfp, nsp) = self.struct_key_meta(id); + let drop_fn = self.value(); + self.emit(Inst::FuncAddr { + out: drop_fn, + function: struct_drop_helper_name(id), + }); let out = self.value(); self.emit(Inst::Call { out: Some(out), function: "mako_native_iface_box".into(), - args: vec![tag_v, value, nf, sm, nm, nfp, nsp], + args: vec![tag_v, value, drop_fn], ret: Some(Type::OwnedOpaque(OpaqueKind::Interface)), }); Ok(out) @@ -29635,17 +29732,10 @@ impl<'a> FunctionLowerer<'a> { args: vec![value], ret: None, }), - Type::Opaque => { - // No-op: the set mixes runtime-allocated blocks with handles - // owned by a foreign library, and nothing distinguishes them. - // See mako_native_opaque_drop in native_bridge.c. - self.emit(Inst::Call { - out: None, - function: "mako_native_opaque_drop".into(), - args: vec![value], - ret: None, - }); - } + // Foreign handles may be interior or library-owned pointers. They + // must be released by their explicit close APIs; a generic free + // here has previously corrupted the allocator. + Type::Opaque => {} Type::Builder => { // Owned by this runtime — struct plus buffer, both reclaimable. self.emit(Inst::Call { @@ -29657,11 +29747,7 @@ impl<'a> FunctionLowerer<'a> { } Type::OwnedOpaque(kind) => self.emit(Inst::Call { out: None, - function: match kind { - OpaqueKind::Interface => "mako_native_iface_drop", - OpaqueKind::HttpRequest => "mako_native_http_request_drop_ptr", - } - .into(), + function: kind.drop_fn().into(), args: vec![value], ret: None, }), @@ -30942,15 +31028,9 @@ impl<'a> FunctionLowerer<'a> { // Handles are not deep-cloned; share the pointer (caller owns). return value; } - Type::OwnedOpaque(OpaqueKind::HttpRequest) => self.emit(Inst::Call { - out: Some(out), - function: "mako_native_http_request_clone_ptr".into(), - args: vec![value], - ret: Some(ty), - }), - Type::OwnedOpaque(OpaqueKind::Interface) => self.emit(Inst::Call { + Type::OwnedOpaque(kind) => self.emit(Inst::Call { out: Some(out), - function: "mako_native_iface_clone".into(), + function: kind.clone_fn().into(), args: vec![value], ret: Some(ty), }), @@ -31551,21 +31631,32 @@ mod tests { #[test] fn emits_typed_drops_for_owned_handles_and_struct_key_maps() { let source = r#" - struct Counter { n: int } + struct Counter { label: string, n: int } interface Adder { fn add(int) -> int } fn Adder_add(self: Counter, delta: int) -> int { return self.n + delta } + struct Bag { items: []int, tag: string } + interface Summer { fn sum(int) -> int } + fn Summer_sum(self: Bag, delta: int) -> int { + return self.items[0] + delta + } struct Label { text: string, id: int } fn main() { - let counter = Counter { n: 2 } + let counter = Counter { label: "n", n: 2 } let adder: Adder = counter let adder_copy = adder + let bag = Bag { items: [1, 2], tag: "bag" } + let summer: Summer = bag + let summer_copy = summer let request = http_request_parse("GET / HTTP/1.1\r\n\r\n") let request_copy = request let mut values = make(map[Label]int) values[Label { text: "a", id: 1 }] = 1 - print_int(adder_copy.add(len(http_request_method(request_copy))) + len(values)) + print_int( + adder_copy.add(len(http_request_method(request_copy))) + + summer_copy.sum(1) + len(values) + ) } "#; let tokens = Lexer::new(source).tokenize().unwrap(); @@ -31585,6 +31676,108 @@ mod tests { assert!(calls.contains(&"mako_native_iface_drop")); assert!(calls.contains(&"mako_native_http_request_drop_ptr")); assert!(calls.contains(&"mako_native_map_struct_key_drop_ptr")); + + let bag_id = module + .structs + .iter() + .position(|layout| layout.name == "Bag") + .unwrap() as u32; + let helper_name = struct_drop_helper_name(bag_id); + let helper = module + .functions + .iter() + .find(|function| function.name == helper_name) + .expect("interface backing structs need a full drop helper"); + assert!(helper.blocks.iter().flat_map(|block| &block.instructions).any( + |instruction| matches!( + instruction, + Inst::DropStruct { + struct_id, + .. + } if *struct_id == bag_id + ) + )); + assert!(module + .functions + .iter() + .flat_map(|function| &function.blocks) + .flat_map(|block| &block.instructions) + .any(|instruction| matches!( + instruction, + Inst::FuncAddr { function, .. } if function == &helper_name + ))); + } + + #[test] + fn rejects_struct_layouts_too_wide_for_native_metadata() { + let fields = (0..63) + .map(|index| format!("f{index}: int")) + .collect::>() + .join(", "); + let source = format!("struct Wide {{ {fields} }} fn main() {{}}"); + let tokens = Lexer::new(&source).tokenize().unwrap(); + let program = Parser::new(tokens).parse().unwrap(); + let error = lower(&program).unwrap_err(); + assert!(error.to_string().contains("supports at most 62")); + } + + #[test] + fn rejects_too_many_nested_struct_metadata_fields() { + let source = r#" + struct Inner { value: int } + struct Outer { + a: Inner + b: Inner + c: Inner + d: Inner + e: Inner + } + fn main() {} + "#; + let tokens = Lexer::new(source).tokenize().unwrap(); + let program = Parser::new(tokens).parse().unwrap(); + let error = lower(&program).unwrap_err(); + assert!(error.to_string().contains("supports at most 4")); + } + + #[test] + fn rejects_nested_structs_too_wide_for_packed_metadata() { + let fields = (0..17) + .map(|index| format!("f{index}: int")) + .collect::>() + .join(", "); + let source = + format!("struct Inner {{ {fields} }} struct Outer {{ inner: Inner }} fn main() {{}}"); + let tokens = Lexer::new(&source).tokenize().unwrap(); + let program = Parser::new(tokens).parse().unwrap(); + let error = lower(&program).unwrap_err(); + assert!(error.to_string().contains("supports at most 16")); + } + + #[test] + fn leaves_foreign_handles_on_their_explicit_close_path() { + let source = r#" + fn main() { + let db = sql_open_sqlite(":memory:") + sql_close(db) + } + "#; + let tokens = Lexer::new(source).tokenize().unwrap(); + let program = Parser::new(tokens).parse().unwrap(); + let module = lower(&program).unwrap(); + let calls = module + .functions + .iter() + .flat_map(|function| &function.blocks) + .flat_map(|block| &block.instructions) + .filter_map(|instruction| match instruction { + Inst::Call { function, .. } => Some(function.as_str()), + _ => None, + }) + .collect::>(); + assert!(calls.contains(&"mako_native_sql_open_sqlite_ptr")); + assert!(calls.contains(&"mako_native_sql_close")); + assert!(!calls.iter().any(|name| name.contains("opaque_drop"))); } } From 3e53559413a842efcb93c4dc0e5114ae9d80ded7 Mon Sep 17 00:00:00 2001 From: "Attah N." Date: Sun, 26 Jul 2026 16:42:05 +0100 Subject: [PATCH 3/4] Handle builders in LLVM lowering --- src/llvm_codegen.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/llvm_codegen.rs b/src/llvm_codegen.rs index a7f46a48..40837ad2 100644 --- a/src/llvm_codegen.rs +++ b/src/llvm_codegen.rs @@ -91,6 +91,7 @@ fn llvm_type<'ctx>(context: &'ctx Context, ty: Type) -> BasicTypeEnum<'ctx> { | Type::Nursery | Type::Task | Type::Opaque + | Type::Builder | Type::OwnedOpaque(_) | Type::FnPtr | Type::StructSlice(_) @@ -355,6 +356,7 @@ fn emit_instruction<'ctx>( | Type::Nursery | Type::Task | Type::Opaque + | Type::Builder | Type::OwnedOpaque(_) | Type::FnPtr | Type::StructSlice(_) From 0c9805f32877d06dce307136f08ed5a958431951 Mon Sep 17 00:00:00 2001 From: "Attah N." Date: Sun, 26 Jul 2026 17:43:25 +0100 Subject: [PATCH 4/4] Preserve native map and slice aliasing --- runtime/native_bridge.c | 35 ++++++++++++--- runtime/native_runtime.c | 95 +++++++++++++++++++++++++++++++++------- src/native_ir.rs | 95 ++++++++++++---------------------------- 3 files changed, 137 insertions(+), 88 deletions(-) diff --git a/runtime/native_bridge.c b/runtime/native_bridge.c index 39407053..b9ef5b21 100644 --- a/runtime/native_bridge.c +++ b/runtime/native_bridge.c @@ -824,17 +824,25 @@ MakoNativePtrSlice *mako_native_ptr_slice_clone_deep(const MakoNativePtrSlice *s return out; } -/* Subslice of pointer array [low:high); owned shallow copy of pointers. */ -MakoNativePtrSlice *mako_native_ptr_slice_slice(const MakoNativePtrSlice *s, int64_t low, int64_t high, int64_t max) { - (void)max; +/* Subslice of pointer array [low:high); non-owning view of the backing array. */ +MakoNativePtrSlice *mako_native_ptr_slice_slice( + const MakoNativePtrSlice *s, int64_t low, int64_t high, int64_t max +) { if (!s) return mako_native_ptr_slice_make(0, 0); int64_t n = (int64_t)s->len; if (low < 0) low = 0; if (high < 0 || high > n) high = n; if (low > high) low = high; - int64_t len = high - low; - MakoNativePtrSlice *out = mako_native_ptr_slice_make(len, len); - for (int64_t i = 0; i < len; ++i) out->data[i] = s->data[low + i]; + int64_t cap = max >= 0 && max <= (int64_t)s->cap + ? max - low + : (int64_t)s->cap - low; + if (cap < high - low) cap = high - low; + MakoNativePtrSlice *out = (MakoNativePtrSlice *)calloc(1, sizeof(*out)); + if (!out) abort(); + out->data = s->data ? s->data + low : NULL; + out->len = (size_t)(high - low); + out->cap = (size_t)cap; + out->owned = 0; return out; } @@ -8527,6 +8535,21 @@ int64_t mako_native_http_request_clone_ptr(int64_t h) { (MakoNativeHttpRequest *)(intptr_t)h ); } +int64_t mako_native_http_request_eq_ptr(int64_t a, int64_t b) { + MakoNativeHttpRequest *left = (MakoNativeHttpRequest *)(intptr_t)a; + MakoNativeHttpRequest *right = (MakoNativeHttpRequest *)(intptr_t)b; + if (left == right) return 1; + if (!left || !right) return 0; + return mako_str_eq( + bridge_borrow_str(left->method), bridge_borrow_str(right->method) + ) && + mako_str_eq( + bridge_borrow_str(left->path), bridge_borrow_str(right->path) + ) && + mako_str_eq( + bridge_borrow_str(left->body), bridge_borrow_str(right->body) + ); +} void mako_native_http_request_drop_ptr(int64_t h) { MakoNativeHttpRequest *box = (MakoNativeHttpRequest *)(intptr_t)h; if (!box) return; diff --git a/runtime/native_runtime.c b/runtime/native_runtime.c index 220f03b7..1d05e8e1 100644 --- a/runtime/native_runtime.c +++ b/runtime/native_runtime.c @@ -1632,6 +1632,7 @@ typedef struct { void *mako_native_iface_clone(void *box); void mako_native_iface_drop(void *box); int64_t mako_native_http_request_clone_ptr(int64_t handle); +int64_t mako_native_http_request_eq_ptr(int64_t a, int64_t b); void mako_native_http_request_drop_ptr(int64_t handle); MakoNativeStructSlice *mako_native_struct_slice_clone_ptr( @@ -2081,7 +2082,67 @@ void mako_native_maps_clear_struct_key( m->tombs = 0; } -/* val_kind: 0 = i64 bits, 1 = string content, 2 = nested struct value. */ +static int64_t mako_native_struct_map_value_eq( + int64_t a, int64_t b, int64_t kind, int64_t nfields, int64_t str_mask, + int64_t nest_mask, int64_t nest_nf_pack, int64_t nest_sm_pack +) { + if (a == b) return 1; + if (!a || !b) return 0; + switch (kind) { + case 1: + return mako_native_str_content_eq( + (MakoNativeString *)(intptr_t)a, + (MakoNativeString *)(intptr_t)b + ); + case 2: + return mako_native_struct_key_eq( + (void *)(intptr_t)a, (void *)(intptr_t)b, nfields, str_mask, + nest_mask, nest_nf_pack, nest_sm_pack + ); + case 3: { + MakoNativeIntSlice *sa = (MakoNativeIntSlice *)(intptr_t)a; + MakoNativeIntSlice *sb = (MakoNativeIntSlice *)(intptr_t)b; + return sa->len == sb->len && + (!sa->len || memcmp(sa->data, sb->data, sa->len * sizeof(*sa->data)) == 0); + } + case 4: { + MakoNativeStringSlice *sa = (MakoNativeStringSlice *)(intptr_t)a; + MakoNativeStringSlice *sb = (MakoNativeStringSlice *)(intptr_t)b; + if (sa->len != sb->len) return 0; + for (size_t i = 0; i < sa->len; ++i) { + if (!mako_native_str_content_eq(sa->data[i], sb->data[i])) return 0; + } + return 1; + } + case 5: { + MakoNativeFloatSlice *sa = (MakoNativeFloatSlice *)(intptr_t)a; + MakoNativeFloatSlice *sb = (MakoNativeFloatSlice *)(intptr_t)b; + if (sa->len != sb->len) return 0; + for (size_t i = 0; i < sa->len; ++i) { + if (sa->data[i] != sb->data[i]) return 0; + } + return 1; + } + case 6: { + MakoNativeStructSlice *sa = (MakoNativeStructSlice *)(intptr_t)a; + MakoNativeStructSlice *sb = (MakoNativeStructSlice *)(intptr_t)b; + if (sa->len != sb->len) return 0; + for (size_t i = 0; i < sa->len; ++i) { + if (!mako_native_struct_key_eq( + sa->data[i], sb->data[i], nfields, str_mask, + nest_mask, nest_nf_pack, nest_sm_pack + )) + return 0; + } + return 1; + } + case 8: + return mako_native_http_request_eq_ptr(a, b); + default: + return 0; + } +} + int64_t mako_native_maps_equal_struct_key( const MakoNativeMapII *a, const MakoNativeMapII *b, @@ -2092,7 +2153,10 @@ int64_t mako_native_maps_equal_struct_key( int64_t nest_sm_pack, int64_t val_kind, int64_t val_nfields, - int64_t val_str_mask + int64_t val_str_mask, + int64_t val_nest_mask, + int64_t val_nest_nf_pack, + int64_t val_nest_sm_pack ) { if (!a || !b) return a == b ? 1 : 0; if (mako_native_map_ii_len(a) != mako_native_map_ii_len(b)) return 0; @@ -2110,16 +2174,10 @@ int64_t mako_native_maps_equal_struct_key( int64_t vb = mako_native_map_struct_key_get_ptr( b, k, nfields, str_mask, nest_mask, nest_nf_pack, nest_sm_pack ); - if (val_kind == 1) { - MakoNativeString *sa = (MakoNativeString *)(intptr_t)va; - MakoNativeString *sb = (MakoNativeString *)(intptr_t)vb; - if (!mako_native_str_content_eq(sa, sb)) return 0; - } else if (val_kind == 2) { - void *pa = (void *)(intptr_t)va; - void *pb = (void *)(intptr_t)vb; - if (!mako_native_struct_key_eq(pa, pb, val_nfields, val_str_mask, 0, 0, 0)) - return 0; - } else if (va != vb) { + if (!mako_native_struct_map_value_eq( + va, vb, val_kind, val_nfields, val_str_mask, + val_nest_mask, val_nest_nf_pack, val_nest_sm_pack + )) { return 0; } } @@ -2827,10 +2885,15 @@ MakoNativeStructSlice *mako_native_maps_values_struct_key( size_t j = 0; for (size_t i = 0; i < m->cap; ++i) { if (m->state[i] == MAKO_NMAP_FULL) { - out->data[j++] = (void *)(intptr_t)mako_native_struct_map_value_clone( - m->vals[i], val_kind, val_nfields, val_str_mask, - val_nest_mask, val_nest_nf_pack, val_nest_sm_pack - ); + int64_t value = m->vals[i]; + /* Slice values remain shared views, matching ordinary maps_values. */ + if (val_kind < 3 || val_kind > 6) { + value = mako_native_struct_map_value_clone( + value, val_kind, val_nfields, val_str_mask, + val_nest_mask, val_nest_nf_pack, val_nest_sm_pack + ); + } + out->data[j++] = (void *)(intptr_t)value; } } out->len = j; diff --git a/src/native_ir.rs b/src/native_ir.rs index a63dda1a..91d677e1 100644 --- a/src/native_ir.rs +++ b/src/native_ir.rs @@ -3344,13 +3344,8 @@ impl<'a> FunctionLowerer<'a> { raw_v = f; } // Pointer maps return a borrow of map storage — clone so the - // binding owns an independent value (avoids double-free with - // the map). MapSS get already returns an owned string clone. - // Shared handles (chan/opaque/…) must stay borrowed: clone is - // identity and force-owning would chan_drop while the map - // still holds the same pointer. - // StructKeyStr stores the string pointer as i64 — treat like - // MapSPtr string values (clone for ownership of the binding). + // binding owns an independent value. MapSS get already + // returns an owned string clone. let ptr_map = matches!(mty, Type::MapIPtr(_) | Type::MapSPtr(_)) || (struct_key_sid.is_some() && vty.is_heap()); if (ptr_map || vty == Type::Str && struct_key_sid.is_some()) @@ -7638,13 +7633,12 @@ impl<'a> FunctionLowerer<'a> { } return Ok((f, Type::F64, false)); } - let (fixed, value_owned) = - if vty.is_heap() && !vty.is_shared_handle() { - let clone = self.emit_clone(out, vty); - (self.null_to_empty_heap(clone, vty)?, true) - } else { - (out, false) - }; + let (fixed, value_owned) = if matches!(vty, Type::OwnedOpaque(_)) { + let clone = self.emit_clone(out, vty); + (self.null_to_empty_heap(clone, vty)?, true) + } else { + (self.null_to_empty_heap(out, vty)?, false) + }; if owned { self.emit_drop(slice, sty); } @@ -7699,13 +7693,12 @@ impl<'a> FunctionLowerer<'a> { } return Ok((f, Type::F64, false)); } - let (fixed, value_owned) = - if vty.is_heap() && !vty.is_shared_handle() { - let clone = self.emit_clone(out, vty); - (self.null_to_empty_heap(clone, vty)?, true) - } else { - (out, false) - }; + let (fixed, value_owned) = if matches!(vty, Type::OwnedOpaque(_)) { + let clone = self.emit_clone(out, vty); + (self.null_to_empty_heap(clone, vty)?, true) + } else { + (self.null_to_empty_heap(out, vty)?, false) + }; if owned { self.emit_drop(slice, sty); } @@ -7916,12 +7909,7 @@ impl<'a> FunctionLowerer<'a> { return Ok((clone, sty, true)); } Type::StructSlice(_) | Type::PtrSlice(_) => { - self.emit(Inst::Call { - out: Some(clone), - function: "mako_native_ptr_slice_clone".into(), - args: vec![out], - ret: Some(sty), - }); + let clone = self.emit_clone(out, sty); self.emit_drop(slice, sty); self.emit_drop(out, sty); return Ok((clone, sty, true)); @@ -10108,27 +10096,15 @@ impl<'a> FunctionLowerer<'a> { if let Type::MapIPtr(vk) = at { if let Some(sid) = vk.struct_key_id() { let (nf, sm, nm, nfp, nsp) = self.struct_key_meta(sid); - let (val_kind, val_nf, val_sm) = match vk { - MapValKind::StructKeyStr(_) => ( - self.const_int(1, Type::I64), - self.const_int(0, Type::I64), - self.const_int(0, Type::I64), - ), - MapValKind::StructKeyToStruct(_, vid) => { - let (vnf, vsm, _vnm, _vnfp, _vnsp) = self.struct_key_meta(vid); - (self.const_int(2, Type::I64), vnf, vsm) - } - // Int / float-bits / other scalar values. - _ => ( - self.const_int(0, Type::I64), - self.const_int(0, Type::I64), - self.const_int(0, Type::I64), - ), - }; + let (val_kind, val_nf, val_sm, val_nm, val_nfp, val_nsp) = + self.struct_map_value_meta(vk); self.emit(Inst::Call { out: Some(out), function: "mako_native_maps_equal_struct_key".into(), - args: vec![a, b, nf, sm, nm, nfp, nsp, val_kind, val_nf, val_sm], + args: vec![ + a, b, nf, sm, nm, nfp, nsp, val_kind, val_nf, val_sm, + val_nm, val_nfp, val_nsp, + ], ret: Some(Type::I64), }); if ao { @@ -29787,15 +29763,7 @@ impl<'a> FunctionLowerer<'a> { ret: None, }), Type::PtrSlice(vk) => { - if matches!( - vk, - MapValKind::IntSlice - | MapValKind::StrSlice - | MapValKind::FloatSlice - | MapValKind::Struct(_) - | MapValKind::StructSlice(_) - | MapValKind::OwnedOpaque(_) - ) { + if matches!(vk, MapValKind::OwnedOpaque(_)) { let (vkind, vnf, vsm, vnm, vnfp, vnsp) = self.struct_map_value_meta(vk); self.emit(Inst::Call { @@ -29805,9 +29773,14 @@ impl<'a> FunctionLowerer<'a> { ret: None, }); } else { + let free_elems = matches!(vk.to_type(), Type::Struct(_)); self.emit(Inst::Call { out: None, - function: "mako_native_ptr_slice_drop".into(), + function: if free_elems { + "mako_native_ptr_slice_drop_free_elems".into() + } else { + "mako_native_ptr_slice_drop".into() + }, args: vec![value], ret: None, }); @@ -31056,17 +31029,7 @@ impl<'a> FunctionLowerer<'a> { args: vec![value], ret: Some(ty), }), - Type::PtrSlice(vk) - if matches!( - vk, - MapValKind::IntSlice - | MapValKind::StrSlice - | MapValKind::FloatSlice - | MapValKind::Struct(_) - | MapValKind::StructSlice(_) - | MapValKind::OwnedOpaque(_) - ) => - { + Type::PtrSlice(vk) if matches!(vk, MapValKind::OwnedOpaque(_)) => { let (vkind, vnf, vsm, vnm, vnfp, vnsp) = self.struct_map_value_meta(vk); self.emit(Inst::Call {