Dissecting AI
From the paper: Ship Your Interpreter

Heap-Dependent Ownership for malloc

A fixed write footprint cannot describe how malloc’s writes vary with allocation state; separation logic makes the allocator’s owned memory depend on which blocks are live.

Read the paper ↗
The short version

What it is

  • Heap-dependent ownership The allocator’s permission to use memory depends on which blocks are currently live, so a byte can be available while free and protected after it is returned.
  • Frame rule Memory owned by the caller but not involved in the allocation can be carried through the call unchanged.

Why it matters

A fixed list of writable addresses cannot capture malloc’s changing behavior: an address may be allocator-writable in one request and part of a returned block in another. Describing ownership relative to the live heap lets the proof preserve the interpreter’s other data.

Before getting into the details, here’s the short version: to reason about malloc, don’t give it one permanent list of addresses it may change. Describe the bytes it owns as the free part of the heap, then hand a newly allocated block to the caller. That makes the allocator’s write permission move with the allocation state.

  1. Preserving live data: The interpreter can be building a result while making more allocations. A concrete example is println("n = " + x): it allocates three strings and frees two. The variables and half-built result still need to survive those calls. The allocator’s contract has to explain why.

  2. A fixed footprint breaks: The textbook approach says a function may write a fixed set of addresses, and facts outside that set survive the call. But malloc can touch different addresses depending on the requested size. From the same heap, malloc(32) writes a header at 0x82000238; malloc(64) returns a block spanning [0x82000210, 0x82000250), which contains that address. So one fixed set would have to include the address for the first call and exclude it to protect the second call’s returned block. That is the snag—not merely that the right set is hard to find.

So what if the writable region followed the heap instead of staying fixed? The canonical example is that same header byte: while it is in the allocator’s free arena, the allocator owns it; if it becomes part of a returned block, the caller owns it. Think of free memory as a notebook page the allocator can write on, then hand over with the writing intact. The analogy stops at ownership: real allocator metadata and byte ranges still have to be represented precisely.

  1. Ownership moves, unrelated memory stays put: The allocator owns the arena bytes outside the live blocks. On a successful allocation, the new block leaves that free-memory ownership and becomes the caller’s block; on failure, the free heap remains as it was. In the paper’s notation, the contract is {isHeap H} p:=malloc(n) {(p=0∗isHeap H)∨(isHeap ((p,n)::H)∗blockOwn p n)}\{isHeap\ H\}\ p := malloc(n)\ \{(p=0 \ast isHeap\ H) \lor (isHeap\ ((p,n)::H) \ast blockOwn\ p\ n)\}. In words, either the result is null and the heap ownership is unchanged, or the live-block list gains the result and the caller owns its block. The separating conjunction means these ownership regions are disjoint.

The frame rule is the useful consequence: caller-owned memory that the allocator’s contract does not mention is preserved across the call. It is like keeping your other notebook pages out of the allocator’s hands, though the analogy does not replace the logic’s disjointness requirement. I like this specification because it describes the permission boundary rather than trying to guess every address a call might touch. The tradeoff is that you now have to maintain the heap and ownership bookkeeping. So the question left open is: how much machinery is worth carrying to make that ownership argument compose?

Read the deep dive

Why People Wanted It

The interpreter keeps data in memory while it allocates more memory. A call to println("n = " + x), for example, allocates three strings and frees two; the variables and partially built result must remain intact throughout. To prove that, you need to say what malloc may change and why bytes the caller already owns survive the call.

A conventional specification gives a function a footprint: the fixed set of addresses it may write. For malloc, that is the wrong shape of answer. The allocator’s writes depend on which bytes are still free and which blocks have already been handed out. So the real question is not “which addresses may malloc always modify?” but “how can its permission to write follow the heap’s changing ownership?”

The Core Move

Specify the allocator as owning the arena’s free bytes, then transfer the newly allocated block from that ownership to the caller. The source gives this specification directly:

{ isHeap H }p:=malloc(n){ (p=0∗isHeap H)∨(isHeap ((p,n)::H)∗blockOwn p n) }\{\ isHeap\ H\ \}\quad p := malloc(n)\quad \{\ (p = 0 \ast isHeap\ H) \lor (isHeap\ ((p,n)::H) \ast blockOwn\ p\ n)\ \}

In words: either allocation fails and the allocator still owns the same free heap, or it succeeds, the live-block list gains the new block, and the caller receives ownership of that block. Here ∗\ast separates disjoint memory ownership.

Why No Fixed Footprint Can Describe malloc

Here is the part that gets technical: the concrete counterexample is about one address whose status changes with the requested allocation size. From the same heap, malloc(32) writes a top-chunk header at 0x82000238. But malloc(64) returns [0x82000210, 0x82000250), a block containing that address.

A fixed footprint would have to include 0x82000238 so the first call may write it. Yet it would have to exclude that address so facts about the block returned by the second call remain protected in later calls. Those requirements conflict. The issue is not an awkward choice of set: no single set of addresses captures both cases.

The heap-dependent specification avoids choosing one. isHeap H owns every arena byte outside the live blocks in H; a byte can therefore be writable while free and become caller-owned when it falls inside a newly allocated block. What does that ownership transfer buy for memory the allocator does not mention?

Why the Frame Rule Preserves the Caller’s Other Data

Now the version for readers who want more of the machinery: the separating conjunction says its two assertions hold over disjoint parts of memory. In the successful branch, isHeap ((p,n)::H) describes the allocator’s remaining free arena, while blockOwn p n describes the returned block. In the failure branch, p = 0 leaves isHeap H unchanged.

The frame rule carries any disjoint caller-owned memory through a call unchanged. The allocator’s specification need not enumerate the interpreter’s variables or an unfinished string: because those bytes are not part of the allocator-owned free heap or the transferred block, the frame rule preserves them. This is the key distinction from a fixed footprint: the permission boundary is determined by current ownership, not by a global list of addresses.

Minimal Causal Code

This plain loop sketches the ownership update, not the allocator’s concrete implementation. The paper gives no formula for a byte-by-byte allocator algorithm; its stated formula is the separation-logic specification above.

# H is the list of live blocks; free_bytes are the arena bytes outside H.
def malloc_spec(H, free_bytes, n):
    p = choose_fresh_block(free_bytes, n)
    if p is None:
        return 0, H, free_bytes, None

    block = bytes_in(p, n)
    assert block <= free_bytes
    free_bytes = free_bytes - block
    H = H + [(p, n)]
    return p, H, free_bytes, block  # block ownership passes to caller

What You Gain, and What You Lose

You gain a specification that matches allocation’s changing ownership: free arena bytes may be used by the allocator, while a returned block becomes protected caller memory. The frame rule then preserves unrelated caller-owned data without listing it in every allocator contract.

You give up the simplicity of one fixed write set. The contract instead depends on a heap predicate and a live-block description, and proofs must maintain that ownership accounting. In this setting, that richer contract made the memory reasoning tractable: the reported proof work moved from a stalled fixed-footprint approach to a proof that closed after the separation-logic port. This module is about the allocator contract, not the separate refinement from interpreter semantics to compiled code.

Quiz
Why can’t one fixed set of addresses describe what malloc may write?
Because malloc always writes every byte in the arena.
Because the allocator’s writes depend on which bytes are still free and which blocks are live.
Because separation logic cannot describe disjoint memory.
Because the caller’s variables are part of the allocator’s fixed footprint.
Core Takeaway

A malloc contract should follow ownership as free bytes become a caller’s block, not pretend the allocator has one permanent write footprint.