Skip to content

os/mm: Add use-after-free detection to the heap allocator - #7475

Open
seokhun-eom24 wants to merge 1 commit into
Samsung:masterfrom
seokhun-eom24:260806-uaf-detection
Open

os/mm: Add use-after-free detection to the heap allocator#7475
seokhun-eom24 wants to merge 1 commit into
Samsung:masterfrom
seokhun-eom24:260806-uaf-detection

Conversation

@seokhun-eom24

@seokhun-eom24 seokhun-eom24 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fill the data area of a chunk with the 0xA5A5A5A5 poison pattern when it enters the free list, and verify the pattern before the chunk is consumed again.
A mismatch means the chunk was written while it was free, which is a use-after-free by whoever owned it previously.

The pattern is written from mm_addfreechunk(), which every chunk passes through on its way into the free list.

Verification happens wherever a free chunk is consumed:

  • mm_malloc() and mm_memalign(), before the chunk is split
  • mm_free(), mm_realloc() and mm_shrinkchunk(), before a neighbouring free
    chunk is merged or absorbed. Checking here reports the corruption much
    earlier than waiting for the chunk to be handed out again.

On detection the report prints the ERROR line, the node information via mm_dump_node(), and a hex dump of the whole chunk so that the corrupted words stand out against the 0xA5A5A5A5 background.
Only the data area following the free node bookkeeping is covered.
The allocator reuses the first SIZEOF_MM_FREENODE bytes for flink/blink and the free debug info while a chunk is free, so a write there corrupts the free list and is caught by DEBUGASSERT_MM_FREE_NODE instead.
The covered size is capped by CONFIG_DEBUG_MM_UAF_POISON_SIZE to bound the cost added to every free and every allocation.

New configuration options:

  • CONFIG_DEBUG_MM_UAF: enable the detector
  • CONFIG_DEBUG_MM_UAF_POISON_SIZE: bytes to poison per chunk, default 64
  • CONFIG_DEBUG_MM_UAF_PANIC: assert on detection instead of only reporting

[Example Log]

mm_uaf_verify: ERROR: Use after free detected.
mm_dump_node: FREE NODE: addr = 0x63a94030, type = F, size = 112, preceding size = 112
mm_dump_node: FREE NODE owner pid = 29 (hello), allocated by code at addr = 0x0e26d4fb
Dump heap: 0x63a94030 - 0x63a940a0
63a94030: 00000070 0e26d4fb 0000001d 00000070 00000000 63a81088 0e26d785 0707001d
63a94050: deadbeef cafebabe 12345678 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5
63a94070: a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5
63a94090: 07070707 07070707 07070707 07070707 80000070 0e26d503 0000001d 00000070

[Example Log Interpretation]

Address/offset Value Meaning
+0x00 00000070 Preceding chunk size = 112; allocation bit clear (free node)
+0x04 0e26d4fb Allocation caller address
+0x08 0000001d Low 16 bits: PID 29; high 16 bits: memory_state 0
+0x0c 00000070 Current chunk size = 112
+0x10 00000000 Free-list flink
+0x14 63a81088 Free-list blink
+0x18 0e26d785 Free caller address
+0x1c 0707001d Low 16 bits: free PID 29; high 16 bits: reserved 0x0707

The poisoned data area begins at offset +0x20. With the default 64-byte poison size, the range from +0x20 through +0x5f is expected to contain 0xA5A5A5A5. In this example:

0x63a94050 (+0x20): deadbeef  <- overwritten
0x63a94054 (+0x24): cafebabe  <- overwritten
0x63a94058 (+0x28): 12345678  <- overwritten
0x63a9405c (+0x2c): a5a5a5a5  <- intact poison

Note: The metadata interpretation above is best-effort. A write through a stale pointer can overwrite the free-node bookkeeping fields at +0x10 through +0x1f because they overlay the first 16 bytes of the former user area. An underrun or arbitrary write can also corrupt the header at +0x00 through +0x0f. The poison verifier excludes these metadata fields, so size, PID, caller addresses, and free-list links must not be considered trustworthy once metadata corruption is suspected. The fields in this example are internally consistent, but that does not prove they were untouched.

Fill the data area of a chunk with the 0xA5A5A5A5 poison pattern when it enters the free list, and verify the pattern before the chunk is consumed again.
A mismatch means the chunk was written while it was free, which is a use-after-free by whoever owned it previously.

The pattern is written from mm_addfreechunk(), which every chunk passes through on its way into the free list.

Verification happens wherever a free chunk is consumed:
- mm_malloc() and mm_memalign(), before the chunk is split
- mm_free(), mm_realloc() and mm_shrinkchunk(), before a neighbouring free
  chunk is merged or absorbed. Checking here reports the corruption much
  earlier than waiting for the chunk to be handed out again.

On detection the report prints the ERROR line, the node information via mm_dump_node(), and a hex dump of the whole chunk so that the corrupted words stand out against the 0xA5A5A5A5 background.
Only the data area following the free node bookkeeping is covered.
The allocator reuses the first SIZEOF_MM_FREENODE bytes for flink/blink and the free debug info while a chunk is free, so a write there corrupts the free list and is caught by DEBUGASSERT_MM_FREE_NODE instead.
The covered size is capped by CONFIG_DEBUG_MM_UAF_POISON_SIZE to bound the cost added to every free and every allocation.

New configuration options:
- CONFIG_DEBUG_MM_UAF: enable the detector
- CONFIG_DEBUG_MM_UAF_POISON_SIZE: bytes to poison per chunk, default 64
- CONFIG_DEBUG_MM_UAF_PANIC: assert on detection instead of only reporting

[Example Log]
```bash
mm_uaf_verify: ERROR: Use after free detected.
mm_dump_node: FREE NODE: addr = 0x63a94030, type = F, size = 112, preceding size = 112
mm_dump_node: FREE NODE owner pid = 29 (hello), allocated by code at addr = 0x0e26d4fb
Dump heap: 0x63a94030 - 0x63a940a0
63a94030: 00000070 0e26d4fb 0000001d 00000070 00000000 63a81088 0e26d785 0707001d
63a94050: deadbeef cafebabe 12345678 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5
63a94070: a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5
63a94090: 07070707 07070707 07070707 07070707 80000070 0e26d503 0000001d 00000070
```

[Example Log Interpretation]
| Address/offset | Value    | Meaning |
|----------------|----------|---------|
| +0x00          | 00000070 | Preceding chunk size = 112; allocation bit clear (free node) |
| +0x04          | 0e26d4fb | Allocation caller address |
| +0x08          | 0000001d | Low 16 bits: PID 29; high 16 bits: memory_state 0 |
| +0x0c          | 00000070 | Current chunk size = 112 |
| +0x10          | 00000000 | Free-list flink |
| +0x14          | 63a81088 | Free-list blink |
| +0x18          | 0e26d785 | Free caller address |
| +0x1c          | 0707001d | Low 16 bits: free PID 29; high 16 bits: reserved 0x0707 |

The poisoned data area begins at offset +0x20. With the default 64-byte poison size, the range from +0x20 through +0x5f is expected to contain 0xA5A5A5A5. In this example:

```text
0x63a94050 (+0x20): deadbeef  <- overwritten
0x63a94054 (+0x24): cafebabe  <- overwritten
0x63a94058 (+0x28): 12345678  <- overwritten
0x63a9405c (+0x2c): a5a5a5a5  <- intact poison
```

Note: The metadata interpretation above is best-effort. A write through a stale pointer can overwrite the free-node bookkeeping fields at +0x10 through +0x1f because they overlay the first 16 bytes of the former user area. An underrun or arbitrary write can also corrupt the header at +0x00 through +0x0f. The poison verifier excludes these metadata fields, so size, PID, caller addresses, and free-list links must not be considered trustworthy once metadata corruption is suspected. The fields in this example are internally consistent, but that does not prove they were untouched.

Co-Authored-By: Claude
Signed-off-by: seokhun-eom <seokhun.eom@samsung.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant