cloudflare / cloudflare/lol-html

use-after-free: c-api attribute handles are invalidated by mutating the element

Open
#336 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
2.1k
Forks
111
PR merge metrics
No merged PRs in 30d

Description

`lol_html_element_set_attribute` / `_remove_attribute` reallocate the element's attribute `Vec`.
Handles from `lol_html_attributes_iterator_get` / `_next` point into that `Vec`, so mutating during
the same handler leaves them dangling. The header promises the opposite:

```c
// Advances the iterator and returns next attribute.
//
// Returns NULL if iterator has been exhausted.
//
// WARNING: Returned attribute is valid only during the handler
// execution and should never be leaked outside of it.
```

That's a promise of validity for the handler's duration. It doesn't hold. lol_html documents the
same kind of lifetime hazard elsewhere, so the omission here looks like an oversight, not a policy:

```c
// WARNING: Selector SHOULD NOT be deallocated if there are any active rewriter
// builders that accepted it as an argument to ...
```

## PoC

`poc.c`: get the iterator, advance once, add one attribute with a name long enough to force the
`Vec` to grow, keep iterating.

```c
#include
#include
#include "lol_html.h"

static lol_html_rewriter_directive_t on_element(lol_html_element_t *element, void *user_data) {
(void)user_data;

lol_html_attributes_iterator_t *iter = lol_html_attributes_iterator_get(element);

const lol_html_attribute_t *attr = lol_html_attributes_iterator_next(iter);
if (!attr) { fprintf(stderr, "no first attribute\n"); return LOL_HTML_STOP; }

fprintf(stderr, "obtained iterator, first attr ok\n");

int rc = lol_html_element_set_attribute(element, "zzzzzzzzzzzzzzzzzzzz", 20, "1", 1);
fprintf(stderr, "set_attribute rc=%d\n", rc);

attr = lol_html_attributes_iterator_next(iter);
if (attr) {
lol_html_str_t name = lol_html_attribute_name_get(attr);
fprintf(stderr, "read name after mutation: %.*s\n", (int)name.len, name.data);
lol_html_str_free(name);
} else {
fprintf(stderr, "iterator exhausted after mutation\n");
}

lol_html_attributes_iterator_free(iter);

return LOL_HTML_CONTINUE;
}

static void write_stub(const char *chunk, size_t len, void *user_data) {
(void)chunk; (void)len; (void)user_data;
}

int main(void) {
lol_html_selector_t *selector = lol_html_selector_parse("*", 1);
lol_html_rewriter_builder_t *builder = lol_html_rewriter_builder_new();

lol_html_rewriter_builder_add_element_content_handlers(
builder, selector, on_element, NULL, NULL, NULL, NULL, NULL);

lol_html_memory_settings_t mem = {
.preallocated_parsing_buffer_size = 0,
.max_allowed_memory_usage = (size_t)-1,
.graceful_bail_out_on_memory_limit_exceeded = false
};

lol_html_rewriter_t *rewriter = lol_html_rewriter_build(
builder, "UTF-8", 5, mem, write_stub, NULL, true);

if (!rewriter) {
lol_html_str_t err = lol_html_take_last_error();
fprintf(stderr, "build failed: %.*s\n", (int)err.len, err.data);
return 1;
}

const char *html = "

";
fprintf(stderr, "write rc=%d\n", lol_html_rewriter_write(rewriter, html, strlen(html)));
fprintf(stderr, "end rc=%d\n", lol_html_rewriter_end(rewriter));

lol_html_rewriter_free(rewriter);
lol_html_selector_free(selector);
return 0;
}
```

Build against pristine v3.0.1 (`608cc4a`) under ASan:

```sh
cd c-api && RUSTFLAGS="-Zsanitizer=address" cargo +nightly build --release \
--target x86_64-unknown-linux-gnu --target-dir /tmp/asan-pristine
LIB=/tmp/asan-pristine/x86_64-unknown-linux-gnu/release
clang -g -fsanitize=address -I c-api/include poc.c -L$LIB -llolhtml -Wl,-rpath,$LIB -o poc
ASAN_OPTIONS=detect_leaks=0:symbolize=0 ./poc
```

```
obtained iterator, first attr ok
set_attribute rc=0
=================================================================
==2676006==ERROR: AddressSanitizer: heap-use-after-free on address 0x7c3f73fe00c8 at pc 0x7f1f75047ed2
READ of size 8 at 0x7c3f73fe00c8 thread T0
#0 0x7f1f75047ed1 (liblolhtml.so+0xb8ed1)
#1 0x5622255298a0 (poc+0x1558a0)
...
freed by thread T0 here:
#0 0x55cd8ea38b30 (poc+0x10fb30)
#1 0x7f6b8583e01e (liblolhtml.so+0x12c01e)
#2 0x7f6b857cdd13 (liblolhtml.so+0xbbd13)
#3 0x55cd8ea7e845 (poc+0x155845)
previously allocated by thread T0 here:
#0 0x55cd8ea38728 (poc+0x10f728)
#1 0x7f6b8583e21f (liblolhtml.so+0x12c21f)
```

## Impact

## Summary: Use-after-free

Contributor guide

Open the contributing guide

Research direction

Start with the C API declarations in c-api/include/lol_html.h and reproduce the issue using the provided poc.c and ASan build commands. Trace the attribute iterator and element mutation entry points to understand the invalidation, then verify that the documented handler-lifetime guarantee holds without a sanitizer use-after-free.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, rust
Domain
api, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.