Skip to content

htsparse: fix HTML-escape truncation, cache-buffer leak, and stats-throttle reset - #465

Merged
xroche merged 3 commits into
masterfrom
htsparse-t0-bugfixes
Jul 1, 2026
Merged

htsparse: fix HTML-escape truncation, cache-buffer leak, and stats-throttle reset#465
xroche merged 3 commits into
masterfrom
htsparse-t0-bugfixes

Conversation

@xroche

@xroche xroche commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Three independent bugs in htsparse.c's output-buffer and coroutine-context macros, surfaced by a macro audit.

HT_ADD_HTMLESCAPED* reserved strlen*5+1024, sized for &, but escape_for_html_print_full emits 6 bytes (&#xHH;) per high byte. Past ~1023 high bytes the escaper hit its internal cap, truncated the string mid-run, and counted its terminating NUL into the mirrored HTML file. The only _full call site rewrites a link into a 2KB buffer, so a long non-ASCII local path reaches it. The macro now takes a per-function expansion factor (6 for _full), pinned by a new escape-room self-test so the constant cannot drift from what the function emits.

HT_ADD_END's not-modified fast path read the stored digest with cache_readdata, which malloc's a buffer, and never freed it: a leak on every page whose on-disk size already matched the rewritten one.

makestat_time throttles the makestat/maketrack stats to once a minute, but it lived in ENGINE_SET_CONTEXT and is a by-value context field that cannot round-trip through ENGINE_SAVE_CONTEXT. The wait loop re-read it from the load-once baseline every iteration, resetting the local before the next compare, so under -%v/maketrack the throttle never held. It is load-once now.

The two edited macros are hand-aligned; each fix wraps its macro in clang-format off/on so touching one line does not realign every backslash.

xroche and others added 3 commits July 1, 2026 10:50
HT_ADD_HTMLESCAPED_ANY reserved strlen*5+1024 on the assumption that
"&" (5 bytes) is the worst-case expansion. That holds for
escape_for_html_print, but escape_for_html_print_full turns a high byte
into "&#xHH;" (6 bytes). Past ~1023 high bytes the reservation is short,
so the escaper hits its internal cap: it truncates the string mid-run and
its overflow return counts the terminating NUL, which then lands inside
the mirrored HTML file. The only _full call site rewrites a link into a
2KB buffer, so a long non-ASCII local path triggers it.

Give the macro a per-function expansion factor (HTS_HTMLESCAPE_MAXEXP=5,
HTS_HTMLESCAPE_FULL_MAXEXP=6) and pass 6 for the _full variant. A new
escape-room self-test pins each function's real worst-case expansion
against the constant the macro reserves, so the two can't drift again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
The not-modified fast path reads the stored //[HTML-MD5]// digest via
cache_readdata, which malloc's the buffer, but never freed it. Every page
whose on-disk size already matches the freshly rewritten one leaks that
buffer. Free it after the compare.

Wrapped the macro in clang-format off/on: it is hand-aligned and
clang-format realigns every backslash on any edit, churning untouched
lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
makestat_time throttles the makestat/maketrack stats to once per minute:
the wait loop compares time_local() against it and, when it fires, writes
it back to the local. But the field is by-value in the extended context,
so it can't round-trip through ENGINE_SAVE_CONTEXT, while ENGINE_SET_CONTEXT
re-read it from the load-once baseline on every loop iteration. That reset
the local before the next compare, so under -%v / maketrack the throttle
never held and the stats line plus the full back-stack dump were emitted
every iteration.

Drop makestat_time (and the never-changing makestat_fp) from SET_CONTEXT;
they belong to the load-once set. Wrapped the macro in clang-format off/on
for the same backslash-realignment reason as HT_ADD_END.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
@xroche
xroche force-pushed the htsparse-t0-bugfixes branch from 0c36d2b to b804ee2 Compare July 1, 2026 08:52
@xroche
xroche merged commit 4958bb8 into master Jul 1, 2026
15 checks passed
@xroche
xroche deleted the htsparse-t0-bugfixes branch July 27, 2026 18:26
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