cloudflare / cloudflare/lol-html
use-after-free: c-api attribute handles are invalidated by mutating the element
- 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
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