Skip to content

Commit 49f9d1b

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 49f9d1b

3 files changed

Lines changed: 102 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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
with open(os.path.join(doc_dir, "doc.html"), "w") as f:
24+
f.write(DOC)
25+
26+
def configure(self, env, rules):
27+
conf = HttpdConf(env, extras={
28+
f"test1.{env.http_tld}": f"""
29+
<Location "/">
30+
AddOutputFilterByType SUBSTITUTE text/html
31+
{rules}
32+
</Location>
33+
""",
34+
})
35+
conf.add_vhost_test1()
36+
conf.install()
37+
assert env.apache_restart() == 0
38+
39+
def get(self, env, path):
40+
return env.curl_get(env.mkurl("http", "test1", path))
41+
42+
# A rule that empties a whole line, followed by another rule.
43+
@pytest.mark.parametrize("mode", list(MODES), ids=list(MODES))
44+
def test_filters_004_01(self, env, mode):
45+
flags = MODES[mode]
46+
self.configure(env, f"""
47+
Substitute "s/^##.*//{flags}"
48+
Substitute "s/foo/bar/{flags}"
49+
""")
50+
r = self.get(env, "/doc.html")
51+
assert r.response, f"{mode}: no response"
52+
assert r.response["status"] == 200
53+
assert r.response["body"].decode() == "\nbar\n"
54+
55+
# The same with a literal (non-regex) pattern.
56+
@pytest.mark.parametrize("flags", ["nf", "nq"], ids=["literal_flatten", "literal_quick"])
57+
def test_filters_004_05(self, env, flags):
58+
self.configure(env, f"""
59+
Substitute "s/## remove this completely//{flags}"
60+
Substitute "s/foo/bar/{flags}"
61+
""")
62+
r = self.get(env, "/doc.html")
63+
assert r.response, f"{flags}: no response"
64+
assert r.response["status"] == 200
65+
assert r.response["body"].decode() == "\nbar\n"
66+
67+
# Control: the emptying rule on its own.
68+
def test_filters_004_02(self, env):
69+
self.configure(env, 'Substitute "s/^##.*//"')
70+
r = self.get(env, "/doc.html")
71+
assert r.response, "no response"
72+
assert r.response["body"].decode() == "\nfoo\n"
73+
74+
# Control: the first rule does not match, the second one does.
75+
def test_filters_004_03(self, env):
76+
self.configure(env, """
77+
Substitute "s/^nomatch.*//"
78+
Substitute "s/foo/bar/"
79+
""")
80+
r = self.get(env, "/doc.html")
81+
assert r.response, "no response"
82+
assert r.response["body"].decode() == "## remove this completely\nbar\n"
83+
84+
# Control: the first rule replaces the line with something non-empty.
85+
def test_filters_004_04(self, env):
86+
self.configure(env, """
87+
Substitute "s/^##.*/xx/"
88+
Substitute "s/foo/bar/"
89+
""")
90+
r = self.get(env, "/doc.html")
91+
assert r.response, "no response"
92+
assert r.response["body"].decode() == "xx\nbar\n"

0 commit comments

Comments
 (0)