From a48775d1138b786b9c148542f0a2c7567858efc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20Emil=20Schulz=20=C3=98stergaard?= Date: Thu, 9 Apr 2026 11:04:27 +0200 Subject: [PATCH 1/9] Add libFuzzer harnesses for parse_bytes, argc_frame, and frame roundtrip Three fuzz harnesses with ASan+libFuzzer: - fuzz-parse-bytes: exercises parse_bytes() with arbitrary input - fuzz-argc-frame: exercises argc_frame() with random token arrays - fuzz-roundtrip: property-based test that frame_to_buf(parse(frame)) produces deterministic output, including padded frames Includes fuzz dictionary and CMake FUZZ_ENABLE option. --- .gitignore | 2 + CMakeLists.txt | 25 +++ README.md | 39 ++++ fuzz/corpus-argc-frame/data_ascii | Bin 0 -> 16 bytes fuzz/corpus-argc-frame/data_hex | Bin 0 -> 17 bytes fuzz/corpus-argc-frame/data_pattern_cnt | Bin 0 -> 18 bytes fuzz/corpus-argc-frame/double_vlan | Bin 0 -> 32 bytes fuzz/corpus-argc-frame/eth_arp | Bin 0 -> 39 bytes fuzz/corpus-argc-frame/eth_basic | Bin 0 -> 21 bytes fuzz/corpus-argc-frame/eth_coap | Bin 0 -> 35 bytes fuzz/corpus-argc-frame/eth_hsr | Bin 0 -> 42 bytes fuzz/corpus-argc-frame/eth_ign_all | Bin 0 -> 24 bytes fuzz/corpus-argc-frame/eth_ign_fields | Bin 0 -> 45 bytes fuzz/corpus-argc-frame/eth_ign_ipv6 | Bin 0 -> 16 bytes fuzz/corpus-argc-frame/eth_ipv4 | Bin 0 -> 48 bytes fuzz/corpus-argc-frame/eth_ipv4_icmp | Bin 0 -> 31 bytes fuzz/corpus-argc-frame/eth_ipv4_icmp_fields | Bin 0 -> 66 bytes fuzz/corpus-argc-frame/eth_ipv4_igmp | Bin 0 -> 43 bytes fuzz/corpus-argc-frame/eth_ipv4_prp | Bin 0 -> 41 bytes fuzz/corpus-argc-frame/eth_ipv4_tcp | Bin 0 -> 84 bytes fuzz/corpus-argc-frame/eth_ipv4_udp | Bin 0 -> 38 bytes fuzz/corpus-argc-frame/eth_ipv6_mld | Bin 0 -> 50 bytes fuzz/corpus-argc-frame/eth_ipv6_udp | Bin 0 -> 68 bytes fuzz/corpus-argc-frame/eth_mrp_lnk | Bin 0 -> 29 bytes fuzz/corpus-argc-frame/eth_mrp_prop_nack | Bin 0 -> 35 bytes fuzz/corpus-argc-frame/eth_mrp_topo | Bin 0 -> 30 bytes fuzz/corpus-argc-frame/eth_mrp_tst | Bin 0 -> 41 bytes fuzz/corpus-argc-frame/eth_oam_ccm | Bin 0 -> 41 bytes fuzz/corpus-argc-frame/eth_oam_laps | Bin 0 -> 30 bytes fuzz/corpus-argc-frame/eth_oam_lb | Bin 0 -> 28 bytes fuzz/corpus-argc-frame/eth_oam_lt | Bin 0 -> 28 bytes fuzz/corpus-argc-frame/eth_oam_raps | Bin 0 -> 30 bytes fuzz/corpus-argc-frame/eth_opcua | Bin 0 -> 79 bytes fuzz/corpus-argc-frame/eth_profinet_rtc | Bin 0 -> 34 bytes fuzz/corpus-argc-frame/eth_ptp_announce | Bin 0 -> 34 bytes fuzz/corpus-argc-frame/eth_ptp_follow_up | Bin 0 -> 35 bytes fuzz/corpus-argc-frame/eth_ptp_peer_request | Bin 0 -> 38 bytes fuzz/corpus-argc-frame/eth_ptp_peer_resp_fu | Bin 0 -> 49 bytes fuzz/corpus-argc-frame/eth_ptp_peer_response | Bin 0 -> 39 bytes fuzz/corpus-argc-frame/eth_ptp_request | Bin 0 -> 33 bytes fuzz/corpus-argc-frame/eth_ptp_response | Bin 0 -> 34 bytes fuzz/corpus-argc-frame/eth_ptp_sync | Bin 0 -> 30 bytes fuzz/corpus-argc-frame/eth_rtag_ipv4 | Bin 0 -> 31 bytes fuzz/corpus-argc-frame/eth_stag_ctag_ipv4 | Bin 0 -> 76 bytes fuzz/corpus-argc-frame/eth_sv | Bin 0 -> 24 bytes fuzz/corpus-argc-frame/payload_only | Bin 0 -> 12 bytes fuzz/corpus-parse-bytes/bin_byte | 1 + fuzz/corpus-parse-bytes/bin_prefix | 1 + fuzz/corpus-parse-bytes/decimal | 1 + fuzz/corpus-parse-bytes/ethertype_oam | 1 + fuzz/corpus-parse-bytes/ethertype_ptp | 1 + fuzz/corpus-parse-bytes/hex_long | 1 + fuzz/corpus-parse-bytes/hex_prefix | 1 + fuzz/corpus-parse-bytes/int_simple | 1 + fuzz/corpus-parse-bytes/ipv4 | 1 + fuzz/corpus-parse-bytes/ipv4_private | 1 + fuzz/corpus-parse-bytes/ipv6_linklocal | 1 + fuzz/corpus-parse-bytes/ipv6_multicast | 1 + fuzz/corpus-parse-bytes/ipv6_short | 1 + fuzz/corpus-parse-bytes/mac | 1 + fuzz/corpus-parse-bytes/mac_oam | 1 + fuzz/corpus-parse-bytes/octal | 1 + fuzz/corpus-parse-bytes/octal_prefix | 1 + fuzz/corpus-parse-bytes/zero | 1 + fuzz/ef.dict | 204 +++++++++++++++++++ fuzz/fuzz-argc-frame.c | 54 +++++ fuzz/fuzz-parse-bytes.c | 42 ++++ fuzz/fuzz-roundtrip.c | 132 ++++++++++++ 68 files changed, 516 insertions(+) create mode 100644 fuzz/corpus-argc-frame/data_ascii create mode 100644 fuzz/corpus-argc-frame/data_hex create mode 100644 fuzz/corpus-argc-frame/data_pattern_cnt create mode 100644 fuzz/corpus-argc-frame/double_vlan create mode 100644 fuzz/corpus-argc-frame/eth_arp create mode 100644 fuzz/corpus-argc-frame/eth_basic create mode 100644 fuzz/corpus-argc-frame/eth_coap create mode 100644 fuzz/corpus-argc-frame/eth_hsr create mode 100644 fuzz/corpus-argc-frame/eth_ign_all create mode 100644 fuzz/corpus-argc-frame/eth_ign_fields create mode 100644 fuzz/corpus-argc-frame/eth_ign_ipv6 create mode 100644 fuzz/corpus-argc-frame/eth_ipv4 create mode 100644 fuzz/corpus-argc-frame/eth_ipv4_icmp create mode 100644 fuzz/corpus-argc-frame/eth_ipv4_icmp_fields create mode 100644 fuzz/corpus-argc-frame/eth_ipv4_igmp create mode 100644 fuzz/corpus-argc-frame/eth_ipv4_prp create mode 100644 fuzz/corpus-argc-frame/eth_ipv4_tcp create mode 100644 fuzz/corpus-argc-frame/eth_ipv4_udp create mode 100644 fuzz/corpus-argc-frame/eth_ipv6_mld create mode 100644 fuzz/corpus-argc-frame/eth_ipv6_udp create mode 100644 fuzz/corpus-argc-frame/eth_mrp_lnk create mode 100644 fuzz/corpus-argc-frame/eth_mrp_prop_nack create mode 100644 fuzz/corpus-argc-frame/eth_mrp_topo create mode 100644 fuzz/corpus-argc-frame/eth_mrp_tst create mode 100644 fuzz/corpus-argc-frame/eth_oam_ccm create mode 100644 fuzz/corpus-argc-frame/eth_oam_laps create mode 100644 fuzz/corpus-argc-frame/eth_oam_lb create mode 100644 fuzz/corpus-argc-frame/eth_oam_lt create mode 100644 fuzz/corpus-argc-frame/eth_oam_raps create mode 100644 fuzz/corpus-argc-frame/eth_opcua create mode 100644 fuzz/corpus-argc-frame/eth_profinet_rtc create mode 100644 fuzz/corpus-argc-frame/eth_ptp_announce create mode 100644 fuzz/corpus-argc-frame/eth_ptp_follow_up create mode 100644 fuzz/corpus-argc-frame/eth_ptp_peer_request create mode 100644 fuzz/corpus-argc-frame/eth_ptp_peer_resp_fu create mode 100644 fuzz/corpus-argc-frame/eth_ptp_peer_response create mode 100644 fuzz/corpus-argc-frame/eth_ptp_request create mode 100644 fuzz/corpus-argc-frame/eth_ptp_response create mode 100644 fuzz/corpus-argc-frame/eth_ptp_sync create mode 100644 fuzz/corpus-argc-frame/eth_rtag_ipv4 create mode 100644 fuzz/corpus-argc-frame/eth_stag_ctag_ipv4 create mode 100644 fuzz/corpus-argc-frame/eth_sv create mode 100644 fuzz/corpus-argc-frame/payload_only create mode 100644 fuzz/corpus-parse-bytes/bin_byte create mode 100644 fuzz/corpus-parse-bytes/bin_prefix create mode 100644 fuzz/corpus-parse-bytes/decimal create mode 100644 fuzz/corpus-parse-bytes/ethertype_oam create mode 100644 fuzz/corpus-parse-bytes/ethertype_ptp create mode 100644 fuzz/corpus-parse-bytes/hex_long create mode 100644 fuzz/corpus-parse-bytes/hex_prefix create mode 100644 fuzz/corpus-parse-bytes/int_simple create mode 100644 fuzz/corpus-parse-bytes/ipv4 create mode 100644 fuzz/corpus-parse-bytes/ipv4_private create mode 100644 fuzz/corpus-parse-bytes/ipv6_linklocal create mode 100644 fuzz/corpus-parse-bytes/ipv6_multicast create mode 100644 fuzz/corpus-parse-bytes/ipv6_short create mode 100644 fuzz/corpus-parse-bytes/mac create mode 100644 fuzz/corpus-parse-bytes/mac_oam create mode 100644 fuzz/corpus-parse-bytes/octal create mode 100644 fuzz/corpus-parse-bytes/octal_prefix create mode 100644 fuzz/corpus-parse-bytes/zero create mode 100644 fuzz/ef.dict create mode 100644 fuzz/fuzz-argc-frame.c create mode 100644 fuzz/fuzz-parse-bytes.c create mode 100644 fuzz/fuzz-roundtrip.c diff --git a/.gitignore b/.gitignore index 378eac2..d5c14bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ build +build-fuzz +corpus-workdir diff --git a/CMakeLists.txt b/CMakeLists.txt index e489537..0114dc5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.20) option(TEST_ENABLE "Enable tests" off) +option(FUZZ_ENABLE "Enable fuzz targets (requires clang with libFuzzer)" off) if (${TEST_ENABLE}) project(easyframes) @@ -103,3 +104,27 @@ include(CTest) add_test(ef-tests ./ef-tests) add_test(parser-tests.rb ${CMAKE_CURRENT_SOURCE_DIR}/test/parser-tests.rb) endif() + +if (${FUZZ_ENABLE}) + # libFuzzer's runtime is C++; clang may not find GCC's libstdc++. + # Ask GCC for the path as a fallback. + find_library(_STDCXX stdc++) + if (NOT _STDCXX) + execute_process( + COMMAND gcc --print-file-name=libstdc++.so + OUTPUT_VARIABLE _STDCXX_PATH OUTPUT_STRIP_TRAILING_WHITESPACE) + if (_STDCXX_PATH AND NOT _STDCXX_PATH STREQUAL "libstdc++.so") + get_filename_component(_STDCXX_DIR "${_STDCXX_PATH}" DIRECTORY) + endif() + endif() + + foreach(target fuzz-parse-bytes fuzz-argc-frame fuzz-roundtrip) + add_executable(${target} fuzz/${target}.c) + target_link_libraries(${target} libef stdc++) + target_compile_options(${target} PRIVATE -fsanitize=fuzzer,address) + target_link_options(${target} PRIVATE -fsanitize=fuzzer,address) + if (_STDCXX_DIR) + target_link_directories(${target} PRIVATE ${_STDCXX_DIR}) + endif() + endforeach() +endif() diff --git a/README.md b/README.md index bdb0a08..cc88bce 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,42 @@ To learn more, have a look at the help message below. $ sudo make install +# Fuzzing + +Three [libFuzzer](https://llvm.org/docs/LibFuzzer.html) harnesses exercise +ef's parsing logic with AddressSanitizer enabled: + +- **fuzz-parse-bytes**: feeds random strings to `parse_bytes()` (numeric, + hex, IPv4, IPv6, MAC address parsing) +- **fuzz-argc-frame**: splits input on null bytes into argv and calls + `argc_frame()` (all header and field parsers) +- **fuzz-roundtrip**: property-based test: parses a frame, serializes it, + and asserts that the result matches its own receive filter via + `bequal_mask()`. Uses the same input format and corpus as fuzz-argc-frame. + +Build (requires clang): + + $ cmake -S . -B build-fuzz -DFUZZ_ENABLE=ON -DCMAKE_C_COMPILER=clang + $ cmake --build build-fuzz -j$(nproc) + +Run (from build-fuzz/): + + $ cd build-fuzz + $ mkdir -p corpus-workdir + $ ./fuzz-parse-bytes corpus-workdir ../fuzz/corpus-parse-bytes -dict=../fuzz/ef.dict -max_total_time=1200 -jobs=12 -workers=12 + $ ./fuzz-argc-frame corpus-workdir ../fuzz/corpus-argc-frame -dict=../fuzz/ef.dict -max_total_time=1200 -jobs=12 -workers=12 + $ ./fuzz-roundtrip corpus-workdir ../fuzz/corpus-argc-frame -dict=../fuzz/ef.dict -max_total_time=1200 -jobs=12 -workers=12 + +When two corpus directories are given, libFuzzer reads seeds from both +but writes new discoveries only to the first. This keeps the checked-in +seed corpus (`fuzz/corpus-*`) clean. The `corpus-workdir` directory is +gitignored and can be deleted at any time. + +The dictionary helps the fuzzer discover valid keywords. Adjust `-jobs` +and `-workers` to match available cores. Each job writes its own log to +`fuzz-.log`. + +If a crash is found, libFuzzer writes a `crash-` file that can be +reproduced with: + + $ ./fuzz-parse-bytes crash- diff --git a/fuzz/corpus-argc-frame/data_ascii b/fuzz/corpus-argc-frame/data_ascii new file mode 100644 index 0000000000000000000000000000000000000000..26273e5b1f97e3966785a675ec02a3a2c0503f00 GIT binary patch literal 16 XcmYdEEJiN=;1z05r-500000 literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/data_pattern_cnt b/fuzz/corpus-argc-frame/data_pattern_cnt new file mode 100644 index 0000000000000000000000000000000000000000..979dc81b15e3a51a9aab163b094184f4d3f53d17 GIT binary patch literal 18 ZcmYdEEJUYFf=e=NQUw>7%~gWOaP^b3CaKf literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_arp b/fuzz/corpus-argc-frame/eth_arp new file mode 100644 index 0000000000000000000000000000000000000000..5d8e4c5337fad50d976d9f18debe08fafbae6c17 GIT binary patch literal 39 fcmYc-$zVvyO-yD;OS3{n48{ literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_basic b/fuzz/corpus-argc-frame/eth_basic new file mode 100644 index 0000000000000000000000000000000000000000..83be6f2f8c3e4ae16419c3f45abfb81490c91ba7 GIT binary patch literal 21 XcmYc-$zVvyO-yF6vNB{ShA@l(MDGR0 literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_coap b/fuzz/corpus-argc-frame/eth_coap new file mode 100644 index 0000000000000000000000000000000000000000..ebfb0f4032e95ef5ccc8adc72adb31d825519a98 GIT binary patch literal 35 lcmYc-$zVvyO-yF6vNB{ShA@m6G7HK~7)nzL7?Sf73jnl%3N-)# literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_hsr b/fuzz/corpus-argc-frame/eth_hsr new file mode 100644 index 0000000000000000000000000000000000000000..ff5ed4761aee8036d3ac99fd9ee21dc152c43982 GIT binary patch literal 42 scmYc-$zVvyO-yF6vNB{ShA@m6GD;HD88QpXOc;tY3mEl`^o;dP008?7T>t<8 literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_ign_all b/fuzz/corpus-argc-frame/eth_ign_all new file mode 100644 index 0000000000000000000000000000000000000000..f82b31038da4ccf38182ec99447bd8b7fac0636f GIT binary patch literal 24 ZcmYc-$zaG#&tu3eC^G>wN>d6zGyq{z2gm>b literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_ign_fields b/fuzz/corpus-argc-frame/eth_ign_fields new file mode 100644 index 0000000000000000000000000000000000000000..ef608a7d973bbeb1ee5ed780a13aa2757e46ac24 GIT binary patch literal 45 ucmYc-$zVvyO-yFUOwVH|1~IIx3>h*D%1juFGYi0?DVYU~dPaK2dL{rmUJT3t literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_ign_ipv6 b/fuzz/corpus-argc-frame/eth_ign_ipv6 new file mode 100644 index 0000000000000000000000000000000000000000..0215675dec1792f88e534c9ab449c5b5028d3997 GIT binary patch literal 16 UcmYc-$zaG#&tu3eC^G{w04*W~LjV8( literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_ipv4 b/fuzz/corpus-argc-frame/eth_ipv4 new file mode 100644 index 0000000000000000000000000000000000000000..dc4cd3eab0c3f1299d344c59a9553c1af939cb63 GIT binary patch literal 48 ycmYc-$zVvyO-yF6vNB{ShA@m6G7HK~7>Y9s81;Y9s81;Y9s81;Y9sz@jNYIxWq>2&5u6Cj|gtdJTyH literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_ipv6_udp b/fuzz/corpus-argc-frame/eth_ipv6_udp new file mode 100644 index 0000000000000000000000000000000000000000..140157e66ea2ed7172d00439e799549c0cd61a18 GIT binary patch literal 68 zcmYc-$zVvyO-yF6vNB{ShA@m6G7HMg7>Y9sz@jNYIxWq>2&AGkrGTNhAitll_5iJQ9*o3aR~tC{|jjV literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_oam_ccm b/fuzz/corpus-argc-frame/eth_oam_ccm new file mode 100644 index 0000000000000000000000000000000000000000..7a2860f1c2e85c61335e5d90e3566e4275daa0e9 GIT binary patch literal 41 scmYc-$zVvyO-yFAvM{hpHnK7>03l-ohGL)~gO!ybLw;heZgO%i0OjHf00000 literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_oam_laps b/fuzz/corpus-argc-frame/eth_oam_laps new file mode 100644 index 0000000000000000000000000000000000000000..bb0d4778d6113398c2fbce1c5654fc755dad7c61 GIT binary patch literal 30 gcmYc-$zVvyO-yF6vNB{ShA@m6@)L7)a}o=R0fw0f-~a#s literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_oam_lb b/fuzz/corpus-argc-frame/eth_oam_lb new file mode 100644 index 0000000000000000000000000000000000000000..268f003a0f3c8d2d5def4a01fced64e9088b3a46 GIT binary patch literal 28 ecmYc-$zVvyO-yF6vNB{ShA@m6@)L7)bCLjcO$Yt} literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_oam_lt b/fuzz/corpus-argc-frame/eth_oam_lt new file mode 100644 index 0000000000000000000000000000000000000000..fc614a16612f219fb4e4bec56331c071d66b223b GIT binary patch literal 28 ecmYc-$zVvyO-yF6vNB{ShA@m6@)L7)b4mbpUkDHY literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_oam_raps b/fuzz/corpus-argc-frame/eth_oam_raps new file mode 100644 index 0000000000000000000000000000000000000000..f2708b752efc1216b7cc0a6a3677fdc518208ba8 GIT binary patch literal 30 gcmYc-$zVvyO-yF6vNB{ShA@m6@)L7)ixLZp0fw;%<^TWy literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_opcua b/fuzz/corpus-argc-frame/eth_opcua new file mode 100644 index 0000000000000000000000000000000000000000..86253221595665ba2f9324e5a9e587aa06520f03 GIT binary patch literal 79 zcmYc-$zVvyO-yF6vNB{ShA@m6G7HK~7>Y9s81;Eg3jn$23hV#? literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_ptp_peer_request b/fuzz/corpus-argc-frame/eth_ptp_peer_request new file mode 100644 index 0000000000000000000000000000000000000000..8bac58facd6aea4e1ad8b56cfa73befda14bc984 GIT binary patch literal 38 ocmYc-$zVvyO-yF6vNB{ShA@m63Q7ue3sO^ybc<38OH+$W0NhIqDF6Tf literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_ptp_peer_resp_fu b/fuzz/corpus-argc-frame/eth_ptp_peer_resp_fu new file mode 100644 index 0000000000000000000000000000000000000000..b3596c45510aadef9701e5ceb0036b3642710dc8 GIT binary patch literal 49 zcmYc-$zVvyO-yF6vNB{ShA@m63Q7ue3sO^ybc<4p3-a@dQ+3nwb8_;_bxR8Xdb|)} literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_ptp_peer_response b/fuzz/corpus-argc-frame/eth_ptp_peer_response new file mode 100644 index 0000000000000000000000000000000000000000..4aacc877e7c9f95dfd5ae077c7fa904b367626bf GIT binary patch literal 39 pcmYc-$zVvyO-yF6vNB{ShA@m63Q7ue3sO^ybc<4p3-a@dQvvAX437W+ literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_ptp_request b/fuzz/corpus-argc-frame/eth_ptp_request new file mode 100644 index 0000000000000000000000000000000000000000..bd59d65dc852e2865bfa197aab00b10382a8dc84 GIT binary patch literal 33 jcmYc-$zVvyO-yF6vNB{ShA@m63Q7uei&6_qQ;SOgr1c6= literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_ptp_response b/fuzz/corpus-argc-frame/eth_ptp_response new file mode 100644 index 0000000000000000000000000000000000000000..40c1fa0896605ab1c9ccb71a4af6d5fd5c3d29c9 GIT binary patch literal 34 kcmYc-$zVvyO-yF6vNB{ShA@m63Q7uei&BdV^7D#Q0k5cE(JR=}4v!Kj`p*XXEQO`)v SSkHtZC9{B4&rHu;&jJ7}&J+;< literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/eth_sv b/fuzz/corpus-argc-frame/eth_sv new file mode 100644 index 0000000000000000000000000000000000000000..6785413487092f99faa93baca02d65297fd82d21 GIT binary patch literal 24 acmYc-$zVvyO-yF6vNB{ShA@m6ipv03od&M} literal 0 HcmV?d00001 diff --git a/fuzz/corpus-argc-frame/payload_only b/fuzz/corpus-argc-frame/payload_only new file mode 100644 index 0000000000000000000000000000000000000000..9e67a1251b5b5ea76ef1ac820c34f710d5eb0e1a GIT binary patch literal 12 TcmXR&tjx(zOkv1L%`*W29%BSv literal 0 HcmV?d00001 diff --git a/fuzz/corpus-parse-bytes/bin_byte b/fuzz/corpus-parse-bytes/bin_byte new file mode 100644 index 0000000..a8f2f03 --- /dev/null +++ b/fuzz/corpus-parse-bytes/bin_byte @@ -0,0 +1 @@ +0b10110011 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/bin_prefix b/fuzz/corpus-parse-bytes/bin_prefix new file mode 100644 index 0000000..5150595 --- /dev/null +++ b/fuzz/corpus-parse-bytes/bin_prefix @@ -0,0 +1 @@ +0b1111 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/decimal b/fuzz/corpus-parse-bytes/decimal new file mode 100644 index 0000000..105d7d9 --- /dev/null +++ b/fuzz/corpus-parse-bytes/decimal @@ -0,0 +1 @@ +100 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/ethertype_oam b/fuzz/corpus-parse-bytes/ethertype_oam new file mode 100644 index 0000000..6ef580e --- /dev/null +++ b/fuzz/corpus-parse-bytes/ethertype_oam @@ -0,0 +1 @@ +0x8902 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/ethertype_ptp b/fuzz/corpus-parse-bytes/ethertype_ptp new file mode 100644 index 0000000..99c064b --- /dev/null +++ b/fuzz/corpus-parse-bytes/ethertype_ptp @@ -0,0 +1 @@ +0x88f7 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/hex_long b/fuzz/corpus-parse-bytes/hex_long new file mode 100644 index 0000000..e89bbf5 --- /dev/null +++ b/fuzz/corpus-parse-bytes/hex_long @@ -0,0 +1 @@ +0xdeadbeef \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/hex_prefix b/fuzz/corpus-parse-bytes/hex_prefix new file mode 100644 index 0000000..f5b111e --- /dev/null +++ b/fuzz/corpus-parse-bytes/hex_prefix @@ -0,0 +1 @@ +0x0800 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/int_simple b/fuzz/corpus-parse-bytes/int_simple new file mode 100644 index 0000000..7813681 --- /dev/null +++ b/fuzz/corpus-parse-bytes/int_simple @@ -0,0 +1 @@ +5 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/ipv4 b/fuzz/corpus-parse-bytes/ipv4 new file mode 100644 index 0000000..6dda9b4 --- /dev/null +++ b/fuzz/corpus-parse-bytes/ipv4 @@ -0,0 +1 @@ +1.2.3.4 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/ipv4_private b/fuzz/corpus-parse-bytes/ipv4_private new file mode 100644 index 0000000..3fc5c17 --- /dev/null +++ b/fuzz/corpus-parse-bytes/ipv4_private @@ -0,0 +1 @@ +192.168.1.1 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/ipv6_linklocal b/fuzz/corpus-parse-bytes/ipv6_linklocal new file mode 100644 index 0000000..41b6bfe --- /dev/null +++ b/fuzz/corpus-parse-bytes/ipv6_linklocal @@ -0,0 +1 @@ +fe80::1 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/ipv6_multicast b/fuzz/corpus-parse-bytes/ipv6_multicast new file mode 100644 index 0000000..f234b86 --- /dev/null +++ b/fuzz/corpus-parse-bytes/ipv6_multicast @@ -0,0 +1 @@ +ff02::1 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/ipv6_short b/fuzz/corpus-parse-bytes/ipv6_short new file mode 100644 index 0000000..ce2866b --- /dev/null +++ b/fuzz/corpus-parse-bytes/ipv6_short @@ -0,0 +1 @@ +::1 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/mac b/fuzz/corpus-parse-bytes/mac new file mode 100644 index 0000000..9447d2a --- /dev/null +++ b/fuzz/corpus-parse-bytes/mac @@ -0,0 +1 @@ +ff:ff:ff:ff:ff:ff \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/mac_oam b/fuzz/corpus-parse-bytes/mac_oam new file mode 100644 index 0000000..8a88514 --- /dev/null +++ b/fuzz/corpus-parse-bytes/mac_oam @@ -0,0 +1 @@ +01:80:c2:00:00:30 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/octal b/fuzz/corpus-parse-bytes/octal new file mode 100644 index 0000000..6b23a94 --- /dev/null +++ b/fuzz/corpus-parse-bytes/octal @@ -0,0 +1 @@ +0o777 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/octal_prefix b/fuzz/corpus-parse-bytes/octal_prefix new file mode 100644 index 0000000..6b23a94 --- /dev/null +++ b/fuzz/corpus-parse-bytes/octal_prefix @@ -0,0 +1 @@ +0o777 \ No newline at end of file diff --git a/fuzz/corpus-parse-bytes/zero b/fuzz/corpus-parse-bytes/zero new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/fuzz/corpus-parse-bytes/zero @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/fuzz/ef.dict b/fuzz/ef.dict new file mode 100644 index 0000000..65f33e8 --- /dev/null +++ b/fuzz/ef.dict @@ -0,0 +1,204 @@ +# ef keywords helps libFuzzer discover valid parse paths + +# Commands +"help" +"hex" +"ign" +"ignore" +"name" +"pcap" +"rate" +"rep" +"repeat" +"rx" +"tx" + +# Headers Layer 2 +"ctag" +"eth" +"htag" +"prp" +"rtag" +"stag" + +# Headers Layer 3/4 +"arp" +"icmp" +"igmp" +"igmpv3_group" +"ipv4" +"ipv6" +"mld" +"mldv2_group" +"tcp" +"udp" + +# Headers OAM +"oam-ccm" +"oam-laps" +"oam-lb" +"oam-lt" +"oam-raps" + +# Headers MRP +"mrp_lnk" +"mrp_prop_nack" +"mrp_topo" +"mrp_tst" + +# Headers PTP +"ptp-announce" +"ptp-follow-up" +"ptp-peer-request" +"ptp-peer-response" +"ptp-peer-response-follow-up" +"ptp-request" +"ptp-response" +"ptp-sync" +"ptp-tlv-org" +"ptp-tlv-path" + +# Headers Industrial / Application +"coap" +"coap-opt" +"coap-parms" +"opc-ua" +"profinet-rtc" +"sv" + +# Headers Payload / Padding +"data" +"padding" + +# Headers IFH (Injection/Extraction Frame Headers) +"ifh-jr2" +"ifh-oc1" +"ifh-sparx5" +"lp-jr2" +"lp-oc1" +"lp-sparx5" +"sp-jr2" +"sp-oc1" +"sp-sparx5" + +# Field names Ethernet +"dmac" +"smac" +"et" + +# Field names VLAN +"vid" +"pcp" +"dei" +"seqn" +"lanid" +"pathid" +"recv" +"suffix" +"size" + +# Field names IPv4 +"ver" +"ihl" +"dscp" +"ecn" +"id" +"flags" +"offset" +"ttl" +"proto" +"chksum" +"sip" +"dip" + +# Field names IPv6 +"flow" +"next" +"hlim" + +# Field names TCP/UDP +"sport" +"dport" +"len" +"seqn" +"ack" +"doff" +"fin" +"syn" +"rst" +"psh" +"urg" +"win" +"urgp" + +# Field names ARP +"htype" +"ptype" +"hlen" +"plen" +"oper" +"sha" +"spa" +"tha" +"tpa" + +# Field names ICMP +"type" +"code" +"hd" + +# Field names IGMP/MLD +"max_resp" +"ga" +"ng" +"ns" +"qrv" +"qqic" +"rec_type" +"auxlen" + +# Field names PTP +"hdr-messageType" +"hdr-versionPTP" +"hdr-domainNumber" +"hdr-clockId" +"hdr-portNumber" +"hdr-sequenceId" +"hdr-correctionField" +"hdr-flagField" +"hdr-logMessageInterval" + +# Field names OAM +"opcode" +"mel" + +# Parse format keywords +"ascii" +"ascii0" +"pattern" +"zero" +"ones" +"cnt" + +# Numeric formats +"0x" +"0b" +"0o" +"::" +"0x0800" +"0x86dd" +"0x8100" +"0x88a8" +"0x88f7" +"0x8902" +"0x88e1" +"ff:ff:ff:ff:ff:ff" +"01:80:c2:00:00:30" +"01:19:1b:00:00:00" +"1.2.3.4" +"::1" +"ff02::1" +"fe80::1" +"255" +"1000" +"0xdeadbeef" diff --git a/fuzz/fuzz-argc-frame.c b/fuzz/fuzz-argc-frame.c new file mode 100644 index 0000000..fea0799 --- /dev/null +++ b/fuzz/fuzz-argc-frame.c @@ -0,0 +1,54 @@ +#include "ef.h" +#include +#include +#include +#include +#include + +int LLVMFuzzerInitialize(int *argc, char ***argv) +{ + int devnull = open("/dev/null", O_WRONLY); + if (devnull >= 0) + dup2(devnull, 1); + return 0; +} + +/* + * Split fuzz input on null bytes to build an argv array, then feed it + * to argc_frame. This exercises all header and field parsers. + */ +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + const char *argv[64]; + int argc = 0; + char *buf; + frame_t *f; + + if (size < 1 || size > 4096) + return 0; + + /* Work on a mutable copy, ensure it's null-terminated */ + buf = (char *)malloc(size + 1); + if (!buf) + return 0; + + memcpy(buf, data, size); + buf[size] = '\0'; + + /* Split on null bytes into argv */ + argv[argc++] = buf; + for (size_t i = 0; i < size && argc < 63; i++) { + if (buf[i] == '\0') { + argv[argc++] = buf + i + 1; + } + } + + f = frame_alloc(); + if (f) { + argc_frame(argc, argv, f); + frame_free(f); + } + + free(buf); + return 0; +} diff --git a/fuzz/fuzz-parse-bytes.c b/fuzz/fuzz-parse-bytes.c new file mode 100644 index 0000000..29e1ea6 --- /dev/null +++ b/fuzz/fuzz-parse-bytes.c @@ -0,0 +1,42 @@ +#include "ef.h" +#include +#include +#include +#include +#include + +int LLVMFuzzerInitialize(int *argc, char ***argv) +{ + int devnull = open("/dev/null", O_WRONLY); + if (devnull >= 0) + dup2(devnull, 1); + return 0; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + char *s; + buf_t *b; + int bytes; + + /* parse_bytes expects a null-terminated string */ + if (size < 1 || size > 256) + return 0; + + s = (char *)malloc(size + 1); + if (!s) + return 0; + + memcpy(s, data, size); + s[size] = '\0'; + + /* Try several output sizes, exercises different code paths */ + for (bytes = 1; bytes <= 16; bytes <<= 1) { + b = parse_bytes(s, bytes); + if (b) + bfree(b); + } + + free(s); + return 0; +} diff --git a/fuzz/fuzz-roundtrip.c b/fuzz/fuzz-roundtrip.c new file mode 100644 index 0000000..1f87ba2 --- /dev/null +++ b/fuzz/fuzz-roundtrip.c @@ -0,0 +1,132 @@ +#include "ef.h" +#include +#include +#include +#include +#include + +int LLVMFuzzerInitialize(int *argc, char ***argv) +{ + int devnull = open("/dev/null", O_WRONLY); + if (devnull >= 0) + dup2(devnull, 1); + return 0; +} + +/* + * Property-based fuzz test: a frame must always match its own receive + * filter. Given a fuzz-generated argv (frame spec), we: + * + * 1. Parse it into a frame via argc_frame() + * 2. Serialize to bytes via frame_to_buf() + * 3. Build the mask via frame_mask_to_buf() (if the frame has ign fields) + * 4. Assert bequal_mask(buf, buf, mask, padding_len) == 1 + * + * If this invariant is violated, the fuzzer reports it as a crash, + * meaning either serialization or matching has a bug. + */ +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + const char *argv[64]; + int argc = 0; + char *input; + frame_t *f; + buf_t *buf, *mask; + int res, matched; + + if (size < 1 || size > 4096) + return 0; + + /* Work on a mutable, null-terminated copy */ + input = (char *)malloc(size + 1); + if (!input) + return 0; + + memcpy(input, data, size); + input[size] = '\0'; + + /* Split on null bytes into argv */ + argv[argc++] = input; + for (size_t i = 0; i < size && argc < 63; i++) { + if (input[i] == '\0') { + argv[argc++] = input + i + 1; + } + } + + /* Parse the frame spec */ + f = frame_alloc(); + if (!f) { + free(input); + return 0; + } + + res = argc_frame(argc, argv, f); + if (res <= 0) { + frame_free(f); + free(input); + return 0; + } + + /* Serialize frame to bytes, returns NULL on invalid combinations */ + buf = frame_to_buf(f); + if (!buf) { + frame_free(f); + free(input); + return 0; + } + + /* Build mask (NULL if no ign fields, bequal_mask handles that) */ + mask = f->has_mask ? frame_mask_to_buf(f) : NULL; + + /* + * For padded frames, construct a synthetic RX frame: the serialized + * frame data plus padding_len zero bytes appended. This simulates + * what a real receiver would see, the expected frame with extra + * trailing bytes from the wire. + */ + if (f->padding_len < 0) { + /* + * Negative padding (e.g. integer overflow from "padding 0xdeadbeef"). + * bequal_mask correctly rejects this, not a bug, skip. + */ + if (mask) bfree(mask); + bfree(buf); + frame_free(f); + free(input); + return 0; + } + + if (f->padding_len > 0) { + /* + * Construct a synthetic RX frame: the serialized frame data plus + * padding_len zero bytes. This simulates what a real receiver + * would see, the expected frame with extra trailing bytes. + */ + buf_t *rx = balloc(buf->size + f->padding_len); + if (!rx) { + if (mask) bfree(mask); + bfree(buf); + frame_free(f); + free(input); + return 0; + } + memcpy(rx->data, buf->data, buf->size); + memset(rx->data + buf->size, 0, f->padding_len); + matched = bequal_mask(rx, buf, mask, f->padding_len); + bfree(rx); + } else { + matched = bequal_mask(buf, buf, mask, 0); + } + + if (!matched) { + /* Invariant violated, abort so the fuzzer captures this input */ + abort(); + } + + if (mask) + bfree(mask); + bfree(buf); + frame_free(f); + free(input); + return 0; +} From e17d417c26ae426c7d8b2c3fa234cca131d92b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20Emil=20Schulz=20=C3=98stergaard?= Date: Thu, 9 Apr 2026 10:58:45 +0200 Subject: [PATCH 2/9] fix parse_bytes_hex/binary: heap-buffer-overflow on oversized input When the hex or binary data has more bytes than the output field size, the write pointer is calculated as b->data + size - data_len, which underflows and writes before the allocated buffer. Reject input where the parsed data exceeds the field size instead of blindly computing a negative offset. Found by libFuzzer with AddressSanitizer: input "0x88a83" followed by 78 'A' characters produces 42 bytes of hex data for a 16-byte field, causing a write 26 bytes before the buffer. --- src/ef-parse-bytes.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ef-parse-bytes.c b/src/ef-parse-bytes.c index e3908fd..a2502cf 100644 --- a/src/ef-parse-bytes.c +++ b/src/ef-parse-bytes.c @@ -84,6 +84,11 @@ buf_t *parse_bytes_binary(const char *s, int size) { return 0; } + if (cnt_bits / 8 > size) { + po("ERROR: binary value too large for %d byte field\n", size); + return 0; + } + b = balloc(size); p = s; @@ -153,6 +158,11 @@ buf_t *parse_bytes_hex(const char *s, int size) { return 0; } + if (cnt_nibble / 2 > size) { + po("ERROR: hex value too large for %d byte field\n", size); + return 0; + } + b = balloc(size); p = s; From 8c4e677b5cc55ea0e1d008f5ece13883f5ff942b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20Emil=20Schulz=20=C3=98stergaard?= Date: Sun, 15 Mar 2026 08:30:31 +0100 Subject: [PATCH 3/9] fix argc_cmds: free parsed commands on error path When parsing fails mid-way through multiple commands, the already-parsed commands (frames, buffers, strdup'd names) were never freed. Route error paths through a cleanup label that calls cmd_destruct on all parsed commands before returning. Found with AddressSanitizer LeakSanitizer on: ./ef name f eth tx eth_red --- src/ef-args.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ef-args.c b/src/ef-args.c index d42bae6..6663b0d 100644 --- a/src/ef-args.c +++ b/src/ef-args.c @@ -282,14 +282,14 @@ int argc_cmds(int argc, const char *argv[]) { break; } else { - return -1; + goto err; } } if (i != argc) { po("Parse error! arg# %d out of %d, cmd_idx = %d\n", i, argc, cmd_idx); - return -1; + goto err; } capture_all_start(); @@ -318,6 +318,13 @@ int argc_cmds(int argc, const char *argv[]) { } return res; + +err: + for (i = 0; i < cmd_idx; ++i) { + cmd_destruct(&cmds[i]); + } + + return -1; } int TIME_OUT_MS = 100; From 0827b092a59b04f958e2741226d4da2b81149ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20Emil=20Schulz=20=C3=98stergaard?= Date: Sun, 15 Mar 2026 08:42:04 +0100 Subject: [PATCH 4/9] fix ef-mld: replace exit() with return -1 on error mld_fill_defaults called exit() when MLD was used without a preceding IPv6 header, killing the entire process. Return -1 instead so frame_to_buf can propagate the error to callers. Check frame_to_buf return value in argc_cmd so invalid combinations produce a proper error exit rather than silently succeeding. --- src/ef-args.c | 4 ++++ src/ef-mld.c | 10 +++++----- src/ef.c | 3 ++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/ef-args.c b/src/ef-args.c index 6663b0d..4b9e88a 100644 --- a/src/ef-args.c +++ b/src/ef-args.c @@ -253,6 +253,10 @@ int argc_cmd(int argc, const char *argv[], cmd_t *c) { if (c->frame) { c->frame_buf = frame_to_buf(c->frame); + if (!c->frame_buf) { + cmd_destruct(c); + return -1; + } if (c->frame->has_mask) c->frame_mask_buf = frame_mask_to_buf(c->frame); diff --git a/src/ef-mld.c b/src/ef-mld.c index a0ce1e4..b30ffb5 100644 --- a/src/ef-mld.c +++ b/src/ef-mld.c @@ -109,29 +109,29 @@ static int mld_fill_defaults(struct frame *f, int stack_idx) { if (!found) { po("Error: MLD fields must be preceded by an IPv6 header (sip = %p, dip = %p)\n", sip, dip); - exit(-1); + return -1; } sip = find_field(ip_hdr, "sip"); if (!sip) { po("Internal error: \"sip\" field not found in IPv6 header\n"); - exit(-1); + return -1; } dip = find_field(ip_hdr, "dip"); if (!dip) { po("Internal error: \"dip\" field not found in IPv6 header\n"); - exit(-1); + return -1; } if (!sip->val || !sip->val->data || sip->val->size != 16) { po("Error: IPv6 header's SIP is not set or its size is not 16 bytes\n"); - exit(-1); + return -1; } if (!dip->val || !dip->val->data || dip->val->size != 16) { po("Error: IPv6 header's DIP is not set or its size is not 16 bytes\n"); - exit(-1); + return -1; } memcpy(pseudo_hdr.sip, sip->val->data, sizeof(pseudo_hdr.sip)); diff --git a/src/ef.c b/src/ef.c index 5a1d5d9..e858bcb 100644 --- a/src/ef.c +++ b/src/ef.c @@ -469,7 +469,8 @@ buf_t *frame_to_buf(frame_t *f) { for (i = f->stack_size - 1; i >= 0; --i) if (f->stack[i]->frame_fill_defaults) - f->stack[i]->frame_fill_defaults(f, i); + if (f->stack[i]->frame_fill_defaults(f, i) < 0) + return NULL; //po("Stack size: %d\n", f->stack_size); for (i = 0; i < f->stack_size; ++i) { From 9132659fe636de8f1823ae937729eb1c18512bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20Emil=20Schulz=20=C3=98stergaard?= Date: Sun, 15 Mar 2026 09:06:26 +0100 Subject: [PATCH 5/9] fix ef-sv: adjust TLV0 bit_offsets when removing TLV2 in sv_fill_defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by fuzz-roundtrip with input: eth ign ipv6 sv Assertion `8 * b->size >= (size_t)f->bit_width + f->bit_offset + offset * 8' failed in hdr_write_field, called from frame_mask_to_buf → hdr_copy_to_buf_mask. When no TLV2 fields are specified, sv_fill_defaults shrinks the SV header from 22 to 14 bytes and zeros TLV2 bit_widths. But it did not adjust the bit_offsets of the TLV0 fields (tlv0_type, tlv0_len) that follow TLV2 in the field array. They kept their original offsets at bits 160 and 168 (bytes 20-21), well past the shrunken 14-byte header. In frame_to_buf this was harmless because TLV0 has no default values, so hdr_write_field was never called for those fields. But frame_mask_to_buf creates mask values for all non-ignored fields, triggering the assertion when it tried to write at bit 160 in a 14-byte header. Reproducible outside fuzzing: ./ef hex eth ign ipv6 sv Fix: shift TLV0 bit_offsets down by 64 bits (the 8 bytes of TLV2) when TLV2 is removed. --- src/ef-sv.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/ef-sv.c b/src/ef-sv.c index 2b12428..1222fbd 100644 --- a/src/ef-sv.c +++ b/src/ef-sv.c @@ -26,11 +26,20 @@ static int sv_fill_defaults(struct frame *f, int stack_idx) { if (!found) { h->size -= 8; // Bytes - // Also adjust the bit-widths to 0 + // Zero the TLV2 bit-widths so no data is written for them for (i = 0; i < sizeof(tlv2_fields) / sizeof(tlv2_fields[0]); i++) { fld = find_field(h, tlv2_fields[i]); fld->bit_width = 0; } + + // Shift TLV0 bit_offsets down by 64 (the 8 bytes of TLV2 removed). + // Without this, TLV0 fields point past the shrunken header, + // causing an assertion failure in hdr_write_field when + // frame_mask_to_buf builds the receive mask. + fld = find_field(h, "tlv0_type"); + fld->bit_offset -= 64; + fld = find_field(h, "tlv0_len"); + fld->bit_offset -= 64; } return 0; From d189937003dc40ea8d9efd7a57d864fc346de521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20Emil=20Schulz=20=C3=98stergaard?= Date: Sun, 15 Mar 2026 09:17:59 +0100 Subject: [PATCH 6/9] fix bequal_mask: heap over-read in padding region, verify padding is zero Two bugs in bequal_mask when padding > 0: 1. Heap buffer over-read: the comparison loop ran up to rx_frame->size, but expected_frame only has expected_frame->size bytes allocated. Indices beyond that read unallocated heap memory. Affects both the memcmp path (no mask) and the masked byte loop. 2. Padding bytes were never validated. Ethernet padding is zero-filled, so non-zero padding bytes indicate a corrupted or wrong frame. Fix: compare only up to expected_frame->size, then verify trailing padding bytes are all zero. Add 8 unit tests covering padding matching, size checks, masked padding, zero-fill verification, and negative padding rejection. --- CMakeLists.txt | 1 + src/ef-buf.c | 36 ++++++---- test/test-padding.cxx | 154 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 14 deletions(-) create mode 100644 test/test-padding.cxx diff --git a/CMakeLists.txt b/CMakeLists.txt index 0114dc5..18a075c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,7 @@ add_executable(ef-tests test/ef-test.cxx test/ef-tests.cxx test/test-ef-parse-bytes.cxx + test/test-padding.cxx test/ifh-ignore.cxx ) diff --git a/src/ef-buf.c b/src/ef-buf.c index ddcbad2..9cd5fe3 100644 --- a/src/ef-buf.c +++ b/src/ef-buf.c @@ -68,23 +68,31 @@ int bequal_mask(const buf_t *rx_frame, const buf_t *expected_frame, if (rx_frame->size != expected_frame->size + padding) return 0; - if (mask == 0) - return memcmp(rx_frame->data, expected_frame->data, rx_frame->size) == 0; - - // Notice, rx_frame->size may be smaller than expected_frame->size - for (i = 0; i < rx_frame->size; ++i) { - unsigned char a_, b_, m_; - a_ = rx_frame->data[i]; - b_ = expected_frame->data[i]; - if (i >= mask->size) { - m_ = 0xff; - } else { - m_ = mask->data[i]; + if (mask == 0) { + if (memcmp(rx_frame->data, expected_frame->data, + expected_frame->size) != 0) + return 0; + } else { + for (i = 0; i < expected_frame->size; ++i) { + unsigned char a_, b_, m_; + a_ = rx_frame->data[i]; + b_ = expected_frame->data[i]; + if (i >= mask->size) { + m_ = 0xff; + } else { + m_ = mask->data[i]; + } + + if ((a_ & m_) != (b_ & m_)) { + return 0; + } } + } - if ((a_ & m_) != (b_ & m_)) { + // Padding bytes must be zero + for (i = expected_frame->size; i < rx_frame->size; ++i) { + if (rx_frame->data[i] != 0) return 0; - } } return 1; diff --git a/test/test-padding.cxx b/test/test-padding.cxx new file mode 100644 index 0000000..1c0dc6b --- /dev/null +++ b/test/test-padding.cxx @@ -0,0 +1,154 @@ +#include "ef.h" +#include "ef-test.h" +#include "catch_single_include.hxx" + +#include + +// Build a frame and its expected buf/mask from a frame spec +static void build_frame(std::vector spec, + buf_t **out_buf, buf_t **out_mask, + frame_t **out_frame) +{ + frame_t *f = parse_frame_wrap(spec); + REQUIRE(f != NULL); + *out_buf = frame_to_buf(f); + REQUIRE(*out_buf != NULL); + *out_mask = f->has_mask ? frame_mask_to_buf(f) : NULL; + *out_frame = f; +} + +// Create a padded copy of a buffer: original data + pad_len zero bytes +static buf_t *make_padded_rx(const buf_t *expected, int pad_len) +{ + buf_t *rx = balloc(expected->size + pad_len); + REQUIRE(rx != NULL); + memcpy(rx->data, expected->data, expected->size); + memset(rx->data + expected->size, 0, pad_len); + return rx; +} + +TEST_CASE("padding: padded frame matches with correct padding", "[padding]") { + buf_t *expected, *mask; + frame_t *f; + build_frame({"eth", "dmac", "::1", "smac", "::2"}, &expected, &mask, &f); + + // Simulate a 4-byte padded RX frame + buf_t *rx = make_padded_rx(expected, 4); + + CHECK(bequal_mask(rx, expected, mask, 4) == 1); + + bfree(rx); + bfree(expected); + frame_free(f); +} + +TEST_CASE("padding: padded frame fails with wrong padding value", "[padding]") { + buf_t *expected, *mask; + frame_t *f; + build_frame({"eth", "dmac", "::1", "smac", "::2"}, &expected, &mask, &f); + + buf_t *rx = make_padded_rx(expected, 4); + + // Wrong padding value, size check fails + CHECK(bequal_mask(rx, expected, mask, 3) == 0); + CHECK(bequal_mask(rx, expected, mask, 5) == 0); + CHECK(bequal_mask(rx, expected, mask, 0) == 0); + + bfree(rx); + bfree(expected); + frame_free(f); +} + +TEST_CASE("padding: zero padding self-matches", "[padding]") { + buf_t *expected, *mask; + frame_t *f; + build_frame({"eth", "dmac", "::1", "smac", "::2"}, &expected, &mask, &f); + + CHECK(bequal_mask(expected, expected, mask, 0) == 1); + + bfree(expected); + frame_free(f); +} + +TEST_CASE("padding: padded frame with mask matches", "[padding]") { + buf_t *expected, *mask; + frame_t *f; + build_frame({"eth", "dmac", "::1", "smac", "ign"}, &expected, &mask, &f); + + REQUIRE(mask != NULL); + + // Build RX with different smac but 8 bytes of padding + frame_t *f2 = parse_frame_wrap({"eth", "dmac", "::1", "smac", "::ff"}); + buf_t *rx_base = frame_to_buf(f2); + buf_t *rx = make_padded_rx(rx_base, 8); + + CHECK(bequal_mask(rx, expected, mask, 8) == 1); + + bfree(rx); + bfree(rx_base); + frame_free(f2); + bfree(mask); + bfree(expected); + frame_free(f); +} + +TEST_CASE("padding: data mismatch in non-padding region fails", "[padding]") { + buf_t *expected, *mask; + frame_t *f; + build_frame({"eth", "dmac", "::1", "smac", "::2"}, &expected, &mask, &f); + + buf_t *rx = make_padded_rx(expected, 4); + + // Corrupt a byte in the frame data (not the padding) + rx->data[0] ^= 0xff; + + CHECK(bequal_mask(rx, expected, mask, 4) == 0); + + bfree(rx); + bfree(expected); + frame_free(f); +} + +TEST_CASE("padding: zero-filled padding bytes match", "[padding]") { + buf_t *expected, *mask; + frame_t *f; + build_frame({"eth", "dmac", "::1", "smac", "::2"}, &expected, &mask, &f); + + // make_padded_rx fills padding with zeros + buf_t *rx = make_padded_rx(expected, 8); + + CHECK(bequal_mask(rx, expected, mask, 8) == 1); + + bfree(rx); + bfree(expected); + frame_free(f); +} + +TEST_CASE("padding: non-zero padding bytes rejected", "[padding]") { + buf_t *expected, *mask; + frame_t *f; + build_frame({"eth", "dmac", "::1", "smac", "::2"}, &expected, &mask, &f); + + buf_t *rx = make_padded_rx(expected, 8); + + // Non-zero padding bytes must cause match failure + memset(rx->data + expected->size, 0xde, 8); + + CHECK(bequal_mask(rx, expected, mask, 8) == 0); + + bfree(rx); + bfree(expected); + frame_free(f); +} + +TEST_CASE("padding: negative padding rejected", "[padding]") { + buf_t *expected, *mask; + frame_t *f; + build_frame({"eth", "dmac", "::1", "smac", "::2"}, &expected, &mask, &f); + + CHECK(bequal_mask(expected, expected, mask, -1) == 0); + CHECK(bequal_mask(expected, expected, mask, -100) == 0); + + bfree(expected); + frame_free(f); +} From 76eae5003f768061d82db66c130cb183c50b47e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20Emil=20Schulz=20=C3=98stergaard?= Date: Sun, 15 Mar 2026 09:38:12 +0100 Subject: [PATCH 7/9] Validate padding size: reject values above 0xffff The padding parser accepted any uint32 value, allowing nonsensical values like 0x6eadbef6 (~1.8GB) that cause OOM. Cap at 0xffff (65535), the maximum IP packet size. Also cap the fuzz harness to match, guarding against fuzz-generated values that bypass normal CLI parsing. --- src/ef-padding.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ef-padding.c b/src/ef-padding.c index 742af1d..da35106 100644 --- a/src/ef-padding.c +++ b/src/ef-padding.c @@ -12,6 +12,11 @@ static int padding_parser(frame_t *f, hdr_t *hdr, int offset, return -1; } + if (len > 0xffff) { + po("ERROR: padding %u is too large (max 65535)\n", len); + return -1; + } + f->padding_len = len; return 1; From 4d9ce68c0838773a8c68ad2e6a17ed3d25f1e921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20Emil=20Schulz=20=C3=98stergaard?= Date: Sun, 15 Mar 2026 09:47:00 +0100 Subject: [PATCH 8/9] fix hdr_copy_to_buf_: skip zero-width fields in mask/value serialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fuzzer crash: eth ign ipv6 ign ipv6 igmp crash-f86f0f4954bca7d9c3ddc000504958de1c4b6346 Several headers (IGMP, MLD, SV, CoAP) shrink during frame_fill_defaults by zeroing bit_width of removed fields. However their bit_offset values remain at the original (pre-shrink) positions. When frame_mask_to_buf later iterates all fields to build the receive mask, it creates a zero-size mask value for these zero-width fields and calls hdr_write_field, which asserts that bit_offset + bit_width fits within the buffer — but the stale bit_offset points past the shrunken header. For IGMP without v3 query/report fields: h->size shrinks by 8 bytes (from 16 to 8), but fields like rresv (bit_offset=96) and ng (bit_offset=112) keep their original offsets, causing the assertion to fire when the frame buffer is too small to contain those offsets. Fix: skip fields with bit_width == 0 in hdr_copy_to_buf_. A zero-width field has no bits to serialize or mask, so processing it is always a no-op — except for the assertion check, which sees the stale offset and aborts. This is a systemic fix covering all current and future headers that shrink during fill_defaults, rather than patching each fill_defaults individually. --- src/ef.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ef.c b/src/ef.c index e858bcb..2835c39 100644 --- a/src/ef.c +++ b/src/ef.c @@ -415,6 +415,9 @@ static int hdr_copy_to_buf_(hdr_t *hdr, size_t offset, buf_t *buf, int mask) { for (i = 0, f = hdr->fields; i < hdr->fields_size; ++i, ++f) { + if (f->bit_width == 0) + continue; + if (BIT_TO_BYTE(f->bit_width) + offset > buf->size) { //po("Buf over flow\n"); return -1; From fa1e792a7f39e47b7f193310e64bdf6aed8f98b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20Emil=20Schulz=20=C3=98stergaard?= Date: Thu, 9 Apr 2026 11:53:34 +0200 Subject: [PATCH 9/9] fix coap_parse_parms: use-after-free on uninitialized buf_t pointer coap_parse_parms declared buf_t *b without initializing to NULL. When parse_var_bytes returned 0 (e.g. empty input), it did not write to its output parameter, leaving b as a garbage pointer. The subsequent bfree(b) freed an arbitrary address. Initialize b to 0 so bfree is a safe no-op on the error path. Found by libFuzzer with AddressSanitizer: ef hex coap-parms par --- src/ef-coap.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ef-coap.c b/src/ef-coap.c index 50dd5f0..7065589 100644 --- a/src/ef-coap.c +++ b/src/ef-coap.c @@ -80,7 +80,7 @@ buf_t *coap_parse_token(hdr_t *hdr, int hdr_offset, const char *s, int bytes) { int coap_parse_parms(hdr_t *hdr, int hdr_offset, struct field *f, int argc, const char *argv[]){ int res; - buf_t *b, *bb = 0; + buf_t *b = 0, *bb = 0; res = parse_var_bytes(&b, argc, argv);