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..18a075c 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) @@ -95,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 ) @@ -103,3 +105,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 0000000..26273e5 Binary files /dev/null and b/fuzz/corpus-argc-frame/data_ascii differ diff --git a/fuzz/corpus-argc-frame/data_hex b/fuzz/corpus-argc-frame/data_hex new file mode 100644 index 0000000..7a80b4c Binary files /dev/null and b/fuzz/corpus-argc-frame/data_hex differ diff --git a/fuzz/corpus-argc-frame/data_pattern_cnt b/fuzz/corpus-argc-frame/data_pattern_cnt new file mode 100644 index 0000000..979dc81 Binary files /dev/null and b/fuzz/corpus-argc-frame/data_pattern_cnt differ diff --git a/fuzz/corpus-argc-frame/double_vlan b/fuzz/corpus-argc-frame/double_vlan new file mode 100644 index 0000000..0c8ce0c Binary files /dev/null and b/fuzz/corpus-argc-frame/double_vlan differ diff --git a/fuzz/corpus-argc-frame/eth_arp b/fuzz/corpus-argc-frame/eth_arp new file mode 100644 index 0000000..5d8e4c5 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_arp differ diff --git a/fuzz/corpus-argc-frame/eth_basic b/fuzz/corpus-argc-frame/eth_basic new file mode 100644 index 0000000..83be6f2 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_basic differ diff --git a/fuzz/corpus-argc-frame/eth_coap b/fuzz/corpus-argc-frame/eth_coap new file mode 100644 index 0000000..ebfb0f4 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_coap differ diff --git a/fuzz/corpus-argc-frame/eth_hsr b/fuzz/corpus-argc-frame/eth_hsr new file mode 100644 index 0000000..ff5ed47 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_hsr differ diff --git a/fuzz/corpus-argc-frame/eth_ign_all b/fuzz/corpus-argc-frame/eth_ign_all new file mode 100644 index 0000000..f82b310 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ign_all differ diff --git a/fuzz/corpus-argc-frame/eth_ign_fields b/fuzz/corpus-argc-frame/eth_ign_fields new file mode 100644 index 0000000..ef608a7 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ign_fields differ diff --git a/fuzz/corpus-argc-frame/eth_ign_ipv6 b/fuzz/corpus-argc-frame/eth_ign_ipv6 new file mode 100644 index 0000000..0215675 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ign_ipv6 differ diff --git a/fuzz/corpus-argc-frame/eth_ipv4 b/fuzz/corpus-argc-frame/eth_ipv4 new file mode 100644 index 0000000..dc4cd3e Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ipv4 differ diff --git a/fuzz/corpus-argc-frame/eth_ipv4_icmp b/fuzz/corpus-argc-frame/eth_ipv4_icmp new file mode 100644 index 0000000..8e5f0cb Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ipv4_icmp differ diff --git a/fuzz/corpus-argc-frame/eth_ipv4_icmp_fields b/fuzz/corpus-argc-frame/eth_ipv4_icmp_fields new file mode 100644 index 0000000..e7f6808 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ipv4_icmp_fields differ diff --git a/fuzz/corpus-argc-frame/eth_ipv4_igmp b/fuzz/corpus-argc-frame/eth_ipv4_igmp new file mode 100644 index 0000000..eae28ec Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ipv4_igmp differ diff --git a/fuzz/corpus-argc-frame/eth_ipv4_prp b/fuzz/corpus-argc-frame/eth_ipv4_prp new file mode 100644 index 0000000..90c64f4 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ipv4_prp differ diff --git a/fuzz/corpus-argc-frame/eth_ipv4_tcp b/fuzz/corpus-argc-frame/eth_ipv4_tcp new file mode 100644 index 0000000..c33947d Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ipv4_tcp differ diff --git a/fuzz/corpus-argc-frame/eth_ipv4_udp b/fuzz/corpus-argc-frame/eth_ipv4_udp new file mode 100644 index 0000000..1096435 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ipv4_udp differ diff --git a/fuzz/corpus-argc-frame/eth_ipv6_mld b/fuzz/corpus-argc-frame/eth_ipv6_mld new file mode 100644 index 0000000..7249a10 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ipv6_mld differ diff --git a/fuzz/corpus-argc-frame/eth_ipv6_udp b/fuzz/corpus-argc-frame/eth_ipv6_udp new file mode 100644 index 0000000..140157e Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ipv6_udp differ diff --git a/fuzz/corpus-argc-frame/eth_mrp_lnk b/fuzz/corpus-argc-frame/eth_mrp_lnk new file mode 100644 index 0000000..96e7c30 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_mrp_lnk differ diff --git a/fuzz/corpus-argc-frame/eth_mrp_prop_nack b/fuzz/corpus-argc-frame/eth_mrp_prop_nack new file mode 100644 index 0000000..1341c60 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_mrp_prop_nack differ diff --git a/fuzz/corpus-argc-frame/eth_mrp_topo b/fuzz/corpus-argc-frame/eth_mrp_topo new file mode 100644 index 0000000..81f9d07 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_mrp_topo differ diff --git a/fuzz/corpus-argc-frame/eth_mrp_tst b/fuzz/corpus-argc-frame/eth_mrp_tst new file mode 100644 index 0000000..fe944d8 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_mrp_tst differ diff --git a/fuzz/corpus-argc-frame/eth_oam_ccm b/fuzz/corpus-argc-frame/eth_oam_ccm new file mode 100644 index 0000000..7a2860f Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_oam_ccm differ diff --git a/fuzz/corpus-argc-frame/eth_oam_laps b/fuzz/corpus-argc-frame/eth_oam_laps new file mode 100644 index 0000000..bb0d477 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_oam_laps differ diff --git a/fuzz/corpus-argc-frame/eth_oam_lb b/fuzz/corpus-argc-frame/eth_oam_lb new file mode 100644 index 0000000..268f003 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_oam_lb differ diff --git a/fuzz/corpus-argc-frame/eth_oam_lt b/fuzz/corpus-argc-frame/eth_oam_lt new file mode 100644 index 0000000..fc614a1 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_oam_lt differ diff --git a/fuzz/corpus-argc-frame/eth_oam_raps b/fuzz/corpus-argc-frame/eth_oam_raps new file mode 100644 index 0000000..f2708b7 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_oam_raps differ diff --git a/fuzz/corpus-argc-frame/eth_opcua b/fuzz/corpus-argc-frame/eth_opcua new file mode 100644 index 0000000..8625322 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_opcua differ diff --git a/fuzz/corpus-argc-frame/eth_profinet_rtc b/fuzz/corpus-argc-frame/eth_profinet_rtc new file mode 100644 index 0000000..7a2462a Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_profinet_rtc differ diff --git a/fuzz/corpus-argc-frame/eth_ptp_announce b/fuzz/corpus-argc-frame/eth_ptp_announce new file mode 100644 index 0000000..482876f Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ptp_announce differ diff --git a/fuzz/corpus-argc-frame/eth_ptp_follow_up b/fuzz/corpus-argc-frame/eth_ptp_follow_up new file mode 100644 index 0000000..e72f334 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ptp_follow_up differ 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 0000000..8bac58f Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ptp_peer_request differ 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 0000000..b3596c4 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ptp_peer_resp_fu differ 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 0000000..4aacc87 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ptp_peer_response differ diff --git a/fuzz/corpus-argc-frame/eth_ptp_request b/fuzz/corpus-argc-frame/eth_ptp_request new file mode 100644 index 0000000..bd59d65 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ptp_request differ diff --git a/fuzz/corpus-argc-frame/eth_ptp_response b/fuzz/corpus-argc-frame/eth_ptp_response new file mode 100644 index 0000000..40c1fa0 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ptp_response differ diff --git a/fuzz/corpus-argc-frame/eth_ptp_sync b/fuzz/corpus-argc-frame/eth_ptp_sync new file mode 100644 index 0000000..10d98cd Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_ptp_sync differ diff --git a/fuzz/corpus-argc-frame/eth_rtag_ipv4 b/fuzz/corpus-argc-frame/eth_rtag_ipv4 new file mode 100644 index 0000000..da7e9cf Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_rtag_ipv4 differ diff --git a/fuzz/corpus-argc-frame/eth_stag_ctag_ipv4 b/fuzz/corpus-argc-frame/eth_stag_ctag_ipv4 new file mode 100644 index 0000000..66f1603 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_stag_ctag_ipv4 differ diff --git a/fuzz/corpus-argc-frame/eth_sv b/fuzz/corpus-argc-frame/eth_sv new file mode 100644 index 0000000..6785413 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_sv differ diff --git a/fuzz/corpus-argc-frame/payload_only b/fuzz/corpus-argc-frame/payload_only new file mode 100644 index 0000000..9e67a12 Binary files /dev/null and b/fuzz/corpus-argc-frame/payload_only differ 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; +} diff --git a/src/ef-args.c b/src/ef-args.c index d42bae6..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); @@ -282,14 +286,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 +322,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; 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/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); 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-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; 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; 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; diff --git a/src/ef.c b/src/ef.c index 5a1d5d9..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; @@ -469,7 +472,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) { 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); +}