Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,15 @@ 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 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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### General networking primitives (protocol-agnostic)

Expand Down
2 changes: 1 addition & 1 deletion docs/MEMORY_SAFETY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
2 changes: 1 addition & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
109 changes: 109 additions & 0 deletions examples/native/owned_handle_drop/native_owned_handle_drop_test.mko
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
struct Counter {
label: string
n: int
}

interface Adder {
fn add(int) -> int
}

struct AdderHolder {
value: Adder
}

struct Bag {
items: []int
tag: string
}

interface Summer {
fn sum(int) -> int
}

struct SummerHolder {
value: Summer
}

struct RequestHolder {
value: HttpRequest
}

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
}

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
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 {
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
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 {
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,
}
values[Label { text: text, id: n }] = Label {
text: int_to_string(n + 2),
id: n + 2,
}
let copy = maps_clone(values)
let result = copy[Label { text: text, id: n }].id
assert_eq(result, n + 2)
return result
}

fn TestOwnedNativeHandlesDrop() {
let mut i = 0
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_eq(i, 200)
assert_eq(total, 184570)
}
111 changes: 76 additions & 35 deletions runtime/native_bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -1039,29 +1047,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));
Expand Down Expand Up @@ -1737,20 +1722,32 @@ int64_t mako_native_http2_conn_new(void) {
return c ? (int64_t)(intptr_t)c : -1;
}

/* Interface fat pointer: { tag, data* } */
typedef void (*MakoNativeIfaceDrop)(void *);

/* Interface fat pointer with the concrete value's compiler-generated drop. */
typedef struct {
int64_t tag;
void *data;
MakoNativeIfaceDrop drop;
_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, MakoNativeIfaceDrop drop) {
MakoNativeIface *b = (MakoNativeIface *)malloc(sizeof(MakoNativeIface));
if (!b) abort();
b->tag = tag;
b->data = data; /* takes ownership of struct heap block */
b->drop = drop;
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;
}
Expand All @@ -1759,9 +1756,13 @@ 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;
if (b->drop) b->drop(b->data);
free(box);
}

int64_t mako_native_http2_conn_use(int64_t c) {
return mako_http2_conn_use((MakoHttp2Conn *)(intptr_t)c);
Expand Down Expand Up @@ -8507,6 +8508,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));
Expand All @@ -8516,6 +8530,34 @@ 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
);
}
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;
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);
Expand Down Expand Up @@ -10816,4 +10858,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);
}

Loading
Loading