From a0961367bf6f60c1fd3af7432a9705b9a04dc8bd Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Sat, 1 Aug 2026 05:50:48 -0600 Subject: [PATCH 1/2] test/fuzzer: fix AFL harness input handling and restore it to a compiling state The harness has not compiled since 5d398907 (merged 2024-08-27) renamed Transformation::evaluate to transform(std::string&, const Transaction*) and removed Action::evaluate(const std::string&, Transaction*). 4df297b5 (merged 2024-10-07) later changed the fourth parameter of Operator::evaluate from std::shared_ptr to RuleMessage&, breaking op_test as well. Neither change touched test/fuzzer/, and --enable-afl-fuzz defaults to false (configure.ac:284), so no CI job surfaced the breakage. A syntax check of current v3/master reports 38 errors in the file. Independently of that, std::string(read_bytes, 128) selected the fill constructor basic_string(size_type count, CharT ch), so the harness ran against read_bytes copies of the character 128 rather than the input AFL supplied. buf was never read from. The compiler has been reporting this as a -Wconstant-conversion warning since the harness was written in c2d9a153. - Build the input string from buf, and skip iterations where read returns nothing. - Update op_test to the current 3-argument Operator::evaluate. - Port the 37 generated transformation calls to Transformation::transform, constructing the classes directly rather than via Transformation::instantiate, whose t:-prefixed matching does not accept class-cased names and would silently yield base-class no-ops. - Add a single-shot __AFL_LOOP fallback so the harness compiles without afl++ installed, and terminates rather than spinning on empty input. Original find and the input-handling fix by @Easton97-Jens in #3556. --- test/fuzzer/afl_fuzzer.cc | 112 ++++++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 40 deletions(-) diff --git a/test/fuzzer/afl_fuzzer.cc b/test/fuzzer/afl_fuzzer.cc index 93afdef276..111468f997 100644 --- a/test/fuzzer/afl_fuzzer.cc +++ b/test/fuzzer/afl_fuzzer.cc @@ -122,13 +122,41 @@ using namespace modsecurity; #include #include +/** +* __AFL_LOOP is provided by afl-clang-fast. When the harness is built with a +* plain compiler it is absent, so define a single-shot fallback: the binary +* reads stdin once and exits. This keeps the harness compilable (and therefore +* checkable) without afl++ installed. +*/ +#ifndef __AFL_LOOP +inline int afl_loop_fallback() { + static bool consumed = false; + if (consumed) { + return 0; + } + consumed = true; + return 1; +} +#define __AFL_LOOP(x) afl_loop_fallback() +#endif + inline void op_test(const std::string &opName, const std::string &s) { Operator *op = Operator::instantiate(opName, ""); op->init("", nullptr); - op->evaluate(nullptr, nullptr, s, nullptr); + op->evaluate(nullptr, nullptr, s); delete op; } +template +inline void tfn_test(const std::string &tfnName, const std::string &s, + const Transaction *t) { + T tfn(tfnName); + // transform() rewrites in place, so each transformation gets its own copy + // of the fuzzer input rather than the output of the previous one. + std::string value(s); + tfn.transform(value, t); +} + int main(int argc, char** argv) { uint8_t buf[128]; @@ -141,8 +169,12 @@ int main(int argc, char** argv) { // (re-) initialize the library and read new input memset(buf, 0, 128); read_bytes = read(STDIN_FILENO, buf, 128); + if (read_bytes <= 0) { + continue; + } - std::string currentString = std::string(read_bytes, 128); + std::string currentString(reinterpret_cast(buf), + read_bytes); const std::string& s = currentString; #if 0 std::string z = lastString; @@ -158,46 +190,46 @@ int main(int argc, char** argv) { /** * Transformations, generated by: * - * for i in $(grep "class " -Ri src/actions/transformations/* | grep " :" | grep -v "InstantCache" | awk {'print $2'}); do echo $i *$(echo $i | awk '{print tolower($0)}') = new $i\(\"$i\"\)\; $(echo $i | awk '{print tolower($0)}')-\>evaluate\(s, NULL\)\; delete $(echo $i | awk '{print tolower($0)}')\;; done; + * for i in $(grep "class " -Ri src/actions/transformations/* | grep " :" | grep -v "InstantCache" | awk {'print $2'}); do echo tfn_test\<$i\>\(\"$i\", s, t\)\;; done; * */ -Base64Decode *base64decode = new Base64Decode("Base64Decode"); base64decode->evaluate(s, NULL); delete base64decode; -Base64DecodeExt *base64decodeext = new Base64DecodeExt("Base64DecodeExt"); base64decodeext->evaluate(s, NULL); delete base64decodeext; -Base64Encode *base64encode = new Base64Encode("Base64Encode"); base64encode->evaluate(s, NULL); delete base64encode; -CmdLine *cmdline = new CmdLine("CmdLine"); cmdline->evaluate(s, NULL); delete cmdline; -CompressWhitespace *compresswhitespace = new CompressWhitespace("CompressWhitespace"); compresswhitespace->evaluate(s, NULL); delete compresswhitespace; -CssDecode *cssdecode = new CssDecode("CssDecode"); cssdecode->evaluate(s, NULL); delete cssdecode; -EscapeSeqDecode *escapeseqdecode = new EscapeSeqDecode("EscapeSeqDecode"); escapeseqdecode->evaluate(s, NULL); delete escapeseqdecode; -HexDecode *hexdecode = new HexDecode("HexDecode"); hexdecode->evaluate(s, NULL); delete hexdecode; -HexEncode *hexencode = new HexEncode("HexEncode"); hexencode->evaluate(s, NULL); delete hexencode; -HtmlEntityDecode *htmlentitydecode = new HtmlEntityDecode("HtmlEntityDecode"); htmlentitydecode->evaluate(s, NULL); delete htmlentitydecode; -JsDecode *jsdecode = new JsDecode("JsDecode"); jsdecode->evaluate(s, NULL); delete jsdecode; -Length *length = new Length("Length"); length->evaluate(s, NULL); delete length; -LowerCase *lowercase = new LowerCase("LowerCase"); lowercase->evaluate(s, NULL); delete lowercase; -Md5 *md5 = new Md5("Md5"); md5->evaluate(s, NULL); delete md5; -None *none = new None("None"); none->evaluate(s, NULL); delete none; -NormalisePath *normalisepath = new NormalisePath("NormalisePath"); normalisepath->evaluate(s, NULL); delete normalisepath; -NormalisePathWin *normalisepathwin = new NormalisePathWin("NormalisePathWin"); normalisepathwin->evaluate(s, NULL); delete normalisepathwin; -ParityEven7bit *parityeven7bit = new ParityEven7bit("ParityEven7bit"); parityeven7bit->evaluate(s, NULL); delete parityeven7bit; -ParityOdd7bit *parityodd7bit = new ParityOdd7bit("ParityOdd7bit"); parityodd7bit->evaluate(s, NULL); delete parityodd7bit; -ParityZero7bit *parityzero7bit = new ParityZero7bit("ParityZero7bit"); parityzero7bit->evaluate(s, NULL); delete parityzero7bit; -RemoveComments *removecomments = new RemoveComments("RemoveComments"); removecomments->evaluate(s, NULL); delete removecomments; -RemoveCommentsChar *removecommentschar = new RemoveCommentsChar("RemoveCommentsChar"); removecommentschar->evaluate(s, NULL); delete removecommentschar; -RemoveNulls *removenulls = new RemoveNulls("RemoveNulls"); removenulls->evaluate(s, NULL); delete removenulls; -RemoveWhitespace *removewhitespace = new RemoveWhitespace("RemoveWhitespace"); removewhitespace->evaluate(s, NULL); delete removewhitespace; -ReplaceComments *replacecomments = new ReplaceComments("ReplaceComments"); replacecomments->evaluate(s, NULL); delete replacecomments; -ReplaceNulls *replacenulls = new ReplaceNulls("ReplaceNulls"); replacenulls->evaluate(s, NULL); delete replacenulls; -Sha1 *sha1 = new Sha1("Sha1"); sha1->evaluate(s, NULL); delete sha1; -SqlHexDecode *sqlhexdecode = new SqlHexDecode("SqlHexDecode"); sqlhexdecode->evaluate(s, NULL); delete sqlhexdecode; -Transformation *transformation = new Transformation("Transformation"); transformation->evaluate(s, NULL); delete transformation; -Trim *trim = new Trim("Trim"); trim->evaluate(s, NULL); delete trim; -TrimLeft *trimleft = new TrimLeft("TrimLeft"); trimleft->evaluate(s, NULL); delete trimleft; -TrimRight *trimright = new TrimRight("TrimRight"); trimright->evaluate(s, NULL); delete trimright; -UpperCase *uppercase = new UpperCase("UpperCase"); uppercase->evaluate(s, NULL); delete uppercase; -UrlDecode *urldecode = new UrlDecode("UrlDecode"); urldecode->evaluate(s, NULL); delete urldecode; -UrlDecodeUni *urldecodeuni = new UrlDecodeUni("UrlDecodeUni"); urldecodeuni->evaluate(s, NULL); delete urldecodeuni; -UrlEncode *urlencode = new UrlEncode("UrlEncode"); urlencode->evaluate(s, NULL); delete urlencode; -Utf8ToUnicode *utf8tounicode = new Utf8ToUnicode("Utf8ToUnicode"); utf8tounicode->evaluate(s, NULL); delete utf8tounicode; +tfn_test("Base64Decode", s, t); +tfn_test("Base64DecodeExt", s, t); +tfn_test("Base64Encode", s, t); +tfn_test("CmdLine", s, t); +tfn_test("CompressWhitespace", s, t); +tfn_test("CssDecode", s, t); +tfn_test("EscapeSeqDecode", s, t); +tfn_test("HexDecode", s, t); +tfn_test("HexEncode", s, t); +tfn_test("HtmlEntityDecode", s, t); +tfn_test("JsDecode", s, t); +tfn_test("Length", s, t); +tfn_test("LowerCase", s, t); +tfn_test("Md5", s, t); +tfn_test("None", s, t); +tfn_test("NormalisePath", s, t); +tfn_test("NormalisePathWin", s, t); +tfn_test("ParityEven7bit", s, t); +tfn_test("ParityOdd7bit", s, t); +tfn_test("ParityZero7bit", s, t); +tfn_test("RemoveComments", s, t); +tfn_test("RemoveCommentsChar", s, t); +tfn_test("RemoveNulls", s, t); +tfn_test("RemoveWhitespace", s, t); +tfn_test("ReplaceComments", s, t); +tfn_test("ReplaceNulls", s, t); +tfn_test("Sha1", s, t); +tfn_test("SqlHexDecode", s, t); +tfn_test("Transformation", s, t); +tfn_test("Trim", s, t); +tfn_test("TrimLeft", s, t); +tfn_test("TrimRight", s, t); +tfn_test("UpperCase", s, t); +tfn_test("UrlDecode", s, t); +tfn_test("UrlDecodeUni", s, t); +tfn_test("UrlEncode", s, t); +tfn_test("Utf8ToUnicode", s, t); /** From 3e34aac81c285e772e18b3125a2ec410bbd71be8 Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Sat, 1 Aug 2026 13:33:05 -0600 Subject: [PATCH 2/2] test/fuzzer: remove "/*" sequences from comments in the AFL harness The generator commands recorded in the harness comments contained src/actions/transformations/*.h and similar, so a "/*" sat inside a block comment and reads as a nested comment opener. SonarQube reports it as cpp:S1103. Quote the glob when handing it to find, and pass the directory straight to grep -R, which needs no glob at all. Both rewritten forms produce output byte-identical to the originals, so the comments still document exactly what generated the include list and the tfn_test/op_test calls. Comment-only change. The harness behavior is unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- test/fuzzer/afl_fuzzer.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/fuzzer/afl_fuzzer.cc b/test/fuzzer/afl_fuzzer.cc index 111468f997..a4d87cc405 100644 --- a/test/fuzzer/afl_fuzzer.cc +++ b/test/fuzzer/afl_fuzzer.cc @@ -20,7 +20,7 @@ #include "src/actions/transformations/transformation.h" /** - * for i in $(ls -l src/actions/transformations/*.h | awk {'print $9'}); do echo "#include \"$i\""; done; + * for i in $(find src/actions/transformations -name '*.h'); do echo "#include \"$i\""; done; * */ #include "src/actions/transformations/base64_decode.h" @@ -64,7 +64,7 @@ /** - * for i in $(ls -l src/operators/*.h | awk {'print $9'}); do echo "#include \"$i\""; done; + * for i in $(find src/operators -name '*.h'); do echo "#include \"$i\""; done; * */ #include "src/operators/begins_with.h" @@ -190,7 +190,7 @@ int main(int argc, char** argv) { /** * Transformations, generated by: * - * for i in $(grep "class " -Ri src/actions/transformations/* | grep " :" | grep -v "InstantCache" | awk {'print $2'}); do echo tfn_test\<$i\>\(\"$i\", s, t\)\;; done; + * for i in $(grep "class " -Ri src/actions/transformations | grep " :" | grep -v "InstantCache" | awk {'print $2'}); do echo tfn_test\<$i\>\(\"$i\", s, t\)\;; done; * */ tfn_test("Base64Decode", s, t); @@ -235,7 +235,7 @@ tfn_test("Utf8ToUnicode", s, t); /** * Operators, generated by: * - * for i in $(grep "class " -Ri src/operators/* | grep " :" | awk {'print $2'}); do echo $i *$(echo $i | awk '{print tolower($0)}') = new $i\(\"$i\", z, false\)\; $(echo $i | awk '{print tolower($0)}')-\>evaluate\(t, s\)\; delete $(echo $i | awk '{print tolower($0)}')\;; done; + * for i in $(grep "class " -Ri src/operators | grep " :" | awk {'print $2'}); do echo $i *$(echo $i | awk '{print tolower($0)}') = new $i\(\"$i\", z, false\)\; $(echo $i | awk '{print tolower($0)}')-\>evaluate\(t, s\)\; delete $(echo $i | awk '{print tolower($0)}')\;; done; * */ op_test("BeginsWith", s);