Skip to content

Commit 5c82109

Browse files
committed
core: Fix ap_varbuf_regsub() crash on an empty result
* server/util.c (regsub_core): Do not terminate the result when nothing is added and the varbuf has no buffer yet; vb->buf is then the shared read-only empty string. This crashed mod_substitute when a regular expression rule replaced a whole line with an empty string. * test/modules/filters/test_004_substitute_empty.py: New test. PR: 70012
1 parent faadf81 commit 5c82109

3 files changed

Lines changed: 103 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*) core: Fix a crash in ap_varbuf_regsub() when the result is empty and
2+
the buffer has not been allocated yet. This made mod_substitute crash
3+
when a regular expression rule replaced a whole line with an empty
4+
string. PR 70012.
5+
[Arturo Bernal]

‎server/util.c‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,11 @@ static apr_status_t regsub_core(apr_pool_t *p, char **result,
421421
else {
422422
if (vb->strlen == AP_VARBUF_UNKNOWN)
423423
vb->strlen = strlen(vb->buf);
424+
if (!len && !vb->avail) {
425+
/* Nothing to add and no buffer yet, vb->buf is the shared
426+
* empty string which must not be written to. */
427+
return APR_SUCCESS;
428+
}
424429
ap_varbuf_grow(vb, vb->strlen + len);
425430
dst = vb->buf + vb->strlen;
426431
vb->strlen += len;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import os
2+
3+
import pytest
4+
5+
from pyhttpd.conf import HttpdConf
6+
7+
# A substitution may legitimately produce zero bytes. Later rules must still
8+
# see the (now empty) line and continue to work on the following lines.
9+
MODES = {
10+
'regex_default': '',
11+
'regex_flatten': 'f',
12+
'regex_quick': 'q',
13+
}
14+
15+
DOC = "## remove this completely\nfoo\n"
16+
17+
18+
class TestSubstituteEmptyResult:
19+
20+
@pytest.fixture(autouse=True, scope='class')
21+
def _class_scope(self, env):
22+
doc_dir = os.path.join(env.server_dir, "htdocs", "test1")
23+
# Binary, so that no CRLF conversion happens on Windows.
24+
with open(os.path.join(doc_dir, "doc.html"), "wb") as f:
25+
f.write(DOC.encode())
26+
27+
def configure(self, env, rules):
28+
conf = HttpdConf(env, extras={
29+
f"test1.{env.http_tld}": f"""
30+
<Location "/">
31+
AddOutputFilterByType SUBSTITUTE text/html
32+
{rules}
33+
</Location>
34+
""",
35+
})
36+
conf.add_vhost_test1()
37+
conf.install()
38+
assert env.apache_restart() == 0
39+
40+
def get(self, env, path):
41+
return env.curl_get(env.mkurl("http", "test1", path))
42+
43+
# A rule that empties a whole line, followed by another rule.
44+
@pytest.mark.parametrize("mode", list(MODES), ids=list(MODES))
45+
def test_filters_004_01(self, env, mode):
46+
flags = MODES[mode]
47+
self.configure(env, f"""
48+
Substitute "s/^##.*//{flags}"
49+
Substitute "s/foo/bar/{flags}"
50+
""")
51+
r = self.get(env, "/doc.html")
52+
assert r.response, f"{mode}: no response"
53+
assert r.response["status"] == 200
54+
assert r.response["body"].decode() == "\nbar\n"
55+
56+
# The same with a literal (non-regex) pattern.
57+
@pytest.mark.parametrize("flags", ["nf", "nq"], ids=["literal_flatten", "literal_quick"])
58+
def test_filters_004_05(self, env, flags):
59+
self.configure(env, f"""
60+
Substitute "s/## remove this completely//{flags}"
61+
Substitute "s/foo/bar/{flags}"
62+
""")
63+
r = self.get(env, "/doc.html")
64+
assert r.response, f"{flags}: no response"
65+
assert r.response["status"] == 200
66+
assert r.response["body"].decode() == "\nbar\n"
67+
68+
# Control: the emptying rule on its own.
69+
def test_filters_004_02(self, env):
70+
self.configure(env, 'Substitute "s/^##.*//"')
71+
r = self.get(env, "/doc.html")
72+
assert r.response, "no response"
73+
assert r.response["body"].decode() == "\nfoo\n"
74+
75+
# Control: the first rule does not match, the second one does.
76+
def test_filters_004_03(self, env):
77+
self.configure(env, """
78+
Substitute "s/^nomatch.*//"
79+
Substitute "s/foo/bar/"
80+
""")
81+
r = self.get(env, "/doc.html")
82+
assert r.response, "no response"
83+
assert r.response["body"].decode() == "## remove this completely\nbar\n"
84+
85+
# Control: the first rule replaces the line with something non-empty.
86+
def test_filters_004_04(self, env):
87+
self.configure(env, """
88+
Substitute "s/^##.*/xx/"
89+
Substitute "s/foo/bar/"
90+
""")
91+
r = self.get(env, "/doc.html")
92+
assert r.response, "no response"
93+
assert r.response["body"].decode() == "xx\nbar\n"

0 commit comments

Comments
 (0)