With url_must_have_scheme(false), LinkFinder::links runs in time quadratic to the input length on certain plain-text inputs. Every . becomes a trigger, so for each dot the domain scanner runs. find_domain_start walks back only to the nearest label boundary, but find_authority_end then walks forward across the whole following run of authority characters. A character such as ~ sets maybe_host = false and does not break the forward scan, so the scan reads to end of input and then fails, which leaves rewind unmoved, and the next dot repeats the same full forward walk. Tested against linkify 0.11.0.
An input built from the repeating unit a.a~ shows 4x growth in wall-clock time per doubling of input size, measured on a --release build:
bytes links ms ratio
49152 0 417.06 -
98304 0 1703.13 4.08
196608 0 6711.25 3.94
393216 0 27017.38 4.03
A 192 KB input already takes about 6.7 seconds. Plain prose, a plain letter run, and a dotted chain such as a.a. scale linearly on the same harness (about 2x per doubling) and finish in well under a millisecond at these sizes.
Reproducer
Cargo.toml:
[dependencies]
linkify = "0.11.0"
src/main.rs, run with cargo run --release:
use linkify::LinkFinder;
use std::time::Instant;
fn main() {
let mut finder = LinkFinder::new();
finder.url_must_have_scheme(false);
for k in [48usize, 96, 192, 384] {
let n = k * 1024;
let unit = "a.a~";
let mut input = unit.repeat(n / unit.len() + 1);
input.truncate(n);
let t = Instant::now();
let count = finder.links(&input).count();
println!("{:>7} bytes {:>3} links {:>9.2} ms", n, count, t.elapsed().as_secs_f64() * 1000.0);
}
}
Scope
An application that turns on the scheme-less domain mode and feeds it text from an outside source can be pushed into seconds of CPU per request with an input under 200 KB, and worse with a larger one. The growth is quadratic, so a small increase in input size produces a large increase in processing time. That gives a cheap denial-of-service lever against any service scanning user-supplied text in this configuration.
With
url_must_have_scheme(false),LinkFinder::linksruns in time quadratic to the input length on certain plain-text inputs. Every.becomes a trigger, so for each dot the domain scanner runs.find_domain_startwalks back only to the nearest label boundary, butfind_authority_endthen walks forward across the whole following run of authority characters. A character such as~setsmaybe_host = falseand does not break the forward scan, so the scan reads to end of input and then fails, which leavesrewindunmoved, and the next dot repeats the same full forward walk. Tested against linkify 0.11.0.An input built from the repeating unit
a.a~shows 4x growth in wall-clock time per doubling of input size, measured on a--releasebuild:A 192 KB input already takes about 6.7 seconds. Plain prose, a plain letter run, and a dotted chain such as
a.a.scale linearly on the same harness (about 2x per doubling) and finish in well under a millisecond at these sizes.Reproducer
Cargo.toml:src/main.rs, run withcargo run --release:Scope
An application that turns on the scheme-less domain mode and feeds it text from an outside source can be pushed into seconds of CPU per request with an input under 200 KB, and worse with a larger one. The growth is quadratic, so a small increase in input size produces a large increase in processing time. That gives a cheap denial-of-service lever against any service scanning user-supplied text in this configuration.