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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,40 @@ jobs:
RUSTDOCFLAGS: '-D warnings'
run: cargo hack doc --feature-powerset

sanitizers:
name: AddressSanitizer + LeakSanitizer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

# AddressSanitizer (which bundles LeakSanitizer) requires nightly. We use
# clang for the bundled C++ so it shares the LLVM sanitizer runtime that
# rustc links into the test binaries. Note: `cargo +nightly` is used
# explicitly because rust-toolchain.toml pins a stable channel that would
# otherwise override the default toolchain in this directory.
- name: Install nightly toolchain
run: rustup toolchain install nightly --profile minimal

- uses: Swatinem/rust-cache@v2
with:
shared-key: sanitizers
save-if: ${{ github.ref_name == 'main' }}

- run: rustup +nightly show

# `--target` is required so build scripts / proc-macros (host) are not
# instrumented. LeakSanitizer is enabled by default under ASan on Linux;
# this catches regressions like https://github.com/ada-url/rust/issues/101
# where the failed-parse path leaked the allocation from `ada_parse`.
- name: Test under AddressSanitizer + LeakSanitizer
env:
CC: clang
CXX: clang++
ADA_SANITIZE: address
RUSTFLAGS: '-Zsanitizer=address'
ASAN_OPTIONS: 'detect_leaks=1'
run: cargo +nightly test --tests --target x86_64-unknown-linux-gnu

format:
name: Format
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ authors = [
"LongYinan <github@lyn.one>",
"Boshen <boshenc@gmail.com>"
]
version = "3.4.4"
version = "3.4.5"
edition = "2024"
description = "Fast WHATWG Compliant URL parser"
documentation = "https://docs.rs/ada-url"
Expand Down
14 changes: 14 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,5 +194,19 @@ fn main() {
}
}

// Optionally compile the bundled C++ (ada) with a sanitizer. This is used
// by the AddressSanitizer CI job so that FFI allocations (e.g. the result
// `ada_parse` always heap-allocates) are instrumented and leaks/errors
// inside ada itself are caught. Enable with `ADA_SANITIZE=address` and a
// matching `RUSTFLAGS=-Zsanitizer=address`; use a clang toolchain (CC/CXX)
// so the C++ shares the LLVM sanitizer runtime that rustc links in.
println!("cargo:rerun-if-env-changed=ADA_SANITIZE");
if let Ok(sanitizer) = env::var("ADA_SANITIZE")
&& !sanitizer.is_empty()
{
build.flag(format!("-fsanitize={sanitizer}"));
build.flag("-fno-omit-frame-pointer");
}

build.compile("ada");
}
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@ impl Url {
if unsafe { ffi::ada_is_valid(url_aggregator) } {
Ok(url_aggregator.into())
} else {
// `ada_parse`/`ada_parse_with_base` always allocate a result on the
// heap, even when parsing fails. On the success path the pointer is
// owned by `Url` and freed via its `Drop` impl, but on the error path
// we must free it here to avoid leaking the allocation.
unsafe { ffi::ada_free(url_aggregator) };
Err(ParseUrlError { input })
}
}
Expand Down
16 changes: 16 additions & 0 deletions tests/basic_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,22 @@ fn confusing_mess() {
assert_eq!(url.origin(), "http://d:2");
}

/// Regression test for <https://github.com/ada-url/rust/issues/101>.
///
/// `ada_parse`/`ada_parse_with_base` always allocate a result on the heap,
/// even when the input fails to parse. The failed-parse path used to drop the
/// returned pointer without calling `ada_free`, leaking one allocation per
/// call. This test repeatedly parses invalid input on both the base-less and
/// with-base code paths; under LeakSanitizer/Valgrind a regression here shows
/// up as leaked allocations originating in `ada_parse`.
#[test]
fn parse_invalid_url_does_not_leak() {
for _ in 0..1_000 {
assert!(Url::parse("http://[invalid", None).is_err());
assert!(Url::parse("http://[invalid", Some("http://example.com")).is_err());
}
}

#[test]
fn standard_file() {
let url = parse("file:///tmp/mock/path");
Expand Down
Loading