Repository navigation
mod_xml2enc: Fix a use-after-free of carried-over input, and don't abort when the response body fails part way through - #835
Open
alepacheco wants to merge 1 commit into
Conversation
alepacheco
force-pushed
the
xml2enc-error-bucket
branch
from
October 12, 2026 22:55
4c03985 to
900276f
Compare
alepacheco
force-pushed
the
xml2enc-error-bucket
branch
from
October 12, 2026 23:02
900276f to
13641e0
Compare
abort when the response body fails part way through. * modules/filters/mod_xml2enc.c (xml2enc_ffunc): When nothing of the input could be converted, copy the unconsumed bytes into a heap bucket rather than referencing them with a transient bucket: they point into the data of the bucket just read, which is destroyed before the loop goes round again, so the next apr_brigade_flatten() read freed memory. This happens when every read from the handler (e.g. a CGI script) ends in a partial multibyte sequence. Stop waiting for more data to sniff the charset once an error bucket arrives, as for EOS, instead of trying to set it aside: setaside isn't implemented for error buckets, so the ap_assert() aborted the child. mod_proxy_http, mod_proxy_ajp and mod_proxy_uwsgi send an error bucket, but no EOS, when the backend fails part way through the body. When the data ends (EOS or error bucket) after a partial multibyte sequence, drop the sequence and pass on what follows rather than setting everything aside, which aborted on an error bucket in the same way, and held back the EOS so the response never ended. If reading the data to sniff fails (e.g. a CGI script times out), remove the filter and pass the data on as is, and if reading the rest of a partial multibyte sequence fails, log and return the error, as for a failed bucket read, instead of the ap_assert()s aborting the child. * test/modules/filters/test_004_xml2enc.py, test/modules/filters/htdocs/test1/cgi/stall.py, test/modules/filters/htdocs/test1/cgi/stall-big5.py, test/modules/filters/htdocs/test1/cgi/split-big5.py: Add tests. * test/modules/filters/env.py: Load mod_proxy, mod_proxy_http, mod_xml2enc and mod_proxy_html if available.
alepacheco
force-pushed
the
xml2enc-error-bucket
branch
from
October 12, 2026 23:30
13641e0 to
1501e6d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is four fixes in
xml2enc_ffunc(), found while fuzzing the filter chain. The first is a use-after-free; the other three are child-process aborts. All four are triggered by the backend or CGI script failing, which SECURITY.md treats as trusted, so this is a public PR.Use-after-free of carried-over input
When mod_xml2enc converts a charset libxml2 doesn't handle itself (via
apr_xlate, e.g. Big5, Shift_JIS, GB18030), and a whole input buffer converts to nothing — every byte is the first half of a multibyte character — it reinserts the unconsumed bytes at the head of the brigade:bufis the data ofbdestroy, the bucket read earlier this iteration, which is destroyed at the bottom of the loop (apr_bucket_destroy(bdestroy)). The transient bucket still points into it, and the next iteration'sapr_brigade_flatten()reads the freed region:It needs the handler to return the body in several reads that each end mid-character — a CGI script that flushes 8000-byte Big5 chunks does it deterministically. It is normally masked because the freed block is reused, not released; it shows under ASan,
--enable-pool-debug, orMaxMemFree 1. Fix: copy into a heap bucket (apr_bucket_heap_create(..., NULL, ...)) so the reinserted data owns itself. This also covers the case where the input came from the flattenedfixbuf: there the transient pointed into a block-scoped stack array, which the next iteration's flatten then wrote into.Three aborts when the body fails part way through
xml2enc_ffunc()has fourap_assert()s on I/O results. Each aborts the child (AH00102 ... assertion "rv == APR_SUCCESS" failed) when the body fails mid-stream:apr_bucket_setaside()is not implemented for error buckets. Reproducer: a backend sendingContent-Length: 1000and a short body, then closing.ProxyHTMLEnable Onover CGI, a script that writes part of a page then stalls pastCGIScriptTimeoutmakes the CGI bucket's blocking read returnAPR_TIMEUP, soapr_brigade_flatten()fails.The change:
APLOGNO()), and pass the rest on;apr_bucket_read()failure path (AH01447) does.Tests
test/modules/filters/test_004_xml2enc.py(self-contained fake backend + CGI scripts;MaxMemFree 1so the UAF is caught, skips whereapr_xlatelacks Big5). On trunk: a child abort at each of the four asserts, one truncated response, and the use-after-free. All pass with the change.test/modules/filtersandtest/modules/proxypass.2.4.x has the same code for all four. There
ap_proxy_backend_broke()adds an EOS after the error bucket, so the proxy can't reach the first two asserts, but the CGI-timeout ones and the use-after-free look reachable the same way.(see
SECURITY.mdandhttps://www.apache.org/security/#reporting-a-vulnerability).
APLOGNO()tag;numbers are assigned by a committer at merge time (see
docs/log-message-tags/README).changes-entries/*.txtfile is included,following the template in
README.CHANGES(not needed otherwise).test/modules/xxxdirectory matching themodules/xxxmodule source,or
test/modules/corefor core server changes. On Unix, verify thesepass locally with e.g.
make check-pytest PYTEST_DIRS=test/modules/xxx.