diff --git a/projects/go-avahi/README.md b/projects/go-avahi/README.md index 05b650d..d22bb6b 100644 --- a/projects/go-avahi/README.md +++ b/projects/go-avahi/README.md @@ -4,7 +4,10 @@ This directory contains fuzzers for the [`go-avahi`](https://github.com/OpenPrin ## Fuzzers -- `fuzz_domain.go`: Fuzzes the `DomainNormalize` function to validate the CGo boundary and `unsafe.Pointer` usage. +- `fuzz_domain.go`: Targets `DomainNormalize`. +- `fuzz_domain_roundtrip.go`: Verifies round-trip consistency between `DomainFrom` and `DomainSlice`. +- `fuzz_service_name.go`: Tests split/join consistency for service names. +- `fuzz_state_strings.go`: Tests stability of `String()` methods for library states. ## Build with OSS-Fuzz locally: 1. Clone the OSS-Fuzz repo: @@ -24,7 +27,10 @@ cd oss-fuzz python3 infra/helper.py build_fuzzers go-avahi ``` -4. Run the fuzzer: +4. Run a fuzzer: ```bash python3 infra/helper.py run_fuzzer go-avahi fuzz_domain_normalize +python3 infra/helper.py run_fuzzer go-avahi fuzz_domain_roundtrip +python3 infra/helper.py run_fuzzer go-avahi fuzz_service_name +python3 infra/helper.py run_fuzzer go-avahi fuzz_state_strings ``` diff --git a/projects/go-avahi/fuzzer/fuzz_domain_roundtrip.go b/projects/go-avahi/fuzzer/fuzz_domain_roundtrip.go new file mode 100644 index 0000000..38e7a92 --- /dev/null +++ b/projects/go-avahi/fuzzer/fuzz_domain_roundtrip.go @@ -0,0 +1,96 @@ +/* + * Fuzz target for go-avahi's Domain round-trip consistency. + * + * Tests that DomainFrom and DomainSlice are inverse operations, + * DomainEqual is reflexive for valid domains, and DomainToLower / + * DomainToUpper are idempotent. + */ + +package fuzzer + +import ( + "testing" + + "github.com/OpenPrinting/go-avahi" +) + +func FuzzDomainRoundTrip(f *testing.F) { + // Seed corpus: representative domain name strings + f.Add("example.local") + f.Add("printer._ipp._tcp.local") + f.Add("My\\.Printer._ipp._tcp.local") + f.Add("Kyocera ECOSYS M2040dn._ipp._tcp.local") + f.Add("") + + f.Fuzz(func(t *testing.T, data string) { + // 1. Round-trip: DomainSlice → DomainFrom → DomainSlice + labels := avahi.DomainSlice(data) + if labels == nil { + // Invalid domain, skip further checks + return + } + + // Reconstruct the domain from parsed labels + reconstructed := avahi.DomainFrom(labels) + + // Re-parse the reconstructed domain + labels2 := avahi.DomainSlice(reconstructed) + if labels2 == nil { + t.Errorf("DomainSlice(DomainFrom(%q)) returned nil; "+ + "labels=%q reconstructed=%q", + data, labels, reconstructed) + return + } + + // Labels must be identical after round-trip + if len(labels) != len(labels2) { + t.Errorf("Round-trip label count mismatch: %d vs %d; "+ + "input=%q labels=%q reconstructed=%q labels2=%q", + len(labels), len(labels2), + data, labels, reconstructed, labels2) + return + } + + for i := range labels { + if labels[i] != labels2[i] { + t.Errorf("Round-trip label mismatch at %d: %q vs %q; "+ + "input=%q", + i, labels[i], labels2[i], data) + } + } + + // 2. DomainNormalize must equal DomainFrom(DomainSlice(data)) + normalized := avahi.DomainNormalize(data) + if normalized != reconstructed { + t.Errorf("DomainNormalize(%q) = %q != DomainFrom(DomainSlice(%q)) = %q", + data, normalized, data, reconstructed) + } + + // 3. DomainEqual: valid domain must equal itself + if !avahi.DomainEqual(data, data) { + t.Errorf("DomainEqual(%q, %q) = false; expected true", + data, data) + } + + // 4. DomainToLower / DomainToUpper idempotency + lower := avahi.DomainToLower(data) + lowerLower := avahi.DomainToLower(lower) + if lower != lowerLower { + t.Errorf("DomainToLower not idempotent: %q → %q → %q", + data, lower, lowerLower) + } + + upper := avahi.DomainToUpper(data) + upperUpper := avahi.DomainToUpper(upper) + if upper != upperUpper { + t.Errorf("DomainToUpper not idempotent: %q → %q → %q", + data, upper, upperUpper) + } + + // 5. Case-insensitive equality + if !avahi.DomainEqual(lower, upper) { + t.Errorf("DomainEqual(lower=%q, upper=%q) = false", + lower, upper) + } + }) +} diff --git a/projects/go-avahi/fuzzer/fuzz_service_name.go b/projects/go-avahi/fuzzer/fuzz_service_name.go new file mode 100644 index 0000000..b4791d9 --- /dev/null +++ b/projects/go-avahi/fuzzer/fuzz_service_name.go @@ -0,0 +1,62 @@ +/* + * Fuzz target for go-avahi's DomainServiceNameSplit / Join consistency. + * + * Tests that splitting a service name and re-joining it produces a + * name that splits back to the same components. + */ + +package fuzzer + +import ( + "testing" + + "github.com/OpenPrinting/go-avahi" +) + +func FuzzServiceName(f *testing.F) { + // Seed corpus: realistic mDNS service names + f.Add("Kyocera ECOSYS M2040dn._ipp._tcp.local") + f.Add("HP LaserJet._ipps._tcp.local") + f.Add("My\\.Printer._ipp._tcp.example.com") + f.Add("_http._tcp.local") + f.Add("") + + f.Fuzz(func(t *testing.T, data string) { + // Split the input into components + instance, svctype, domain := avahi.DomainServiceNameSplit(data) + + if instance == "" && svctype == "" && domain == "" { + // Invalid service name, nothing more to check + return + } + + // Re-join the components + joined := avahi.DomainServiceNameJoin(instance, svctype, domain) + if joined == "" { + t.Errorf("DomainServiceNameJoin(%q, %q, %q) returned empty; "+ + "input=%q", instance, svctype, domain, data) + return + } + + // Split the re-joined name and verify consistency + instance2, svctype2, domain2 := avahi.DomainServiceNameSplit(joined) + + if instance != instance2 { + t.Errorf("Instance mismatch: %q vs %q; "+ + "input=%q joined=%q", + instance, instance2, data, joined) + } + + if svctype != svctype2 { + t.Errorf("Svctype mismatch: %q vs %q; "+ + "input=%q joined=%q", + svctype, svctype2, data, joined) + } + + if domain != domain2 { + t.Errorf("Domain mismatch: %q vs %q; "+ + "input=%q joined=%q", + domain, domain2, data, joined) + } + }) +} diff --git a/projects/go-avahi/fuzzer/fuzz_state_strings.go b/projects/go-avahi/fuzzer/fuzz_state_strings.go new file mode 100644 index 0000000..998cf1d --- /dev/null +++ b/projects/go-avahi/fuzzer/fuzz_state_strings.go @@ -0,0 +1,54 @@ +/* + * Fuzz target for go-avahi's enum .String() methods. + * + * Tests BrowserEvent, ClientState, EntryGroupState, and ResolverEvent + * String() methods with arbitrary integer values to ensure they never + * panic and always return non-empty strings. + */ + +package fuzzer + +import ( + "testing" + + "github.com/OpenPrinting/go-avahi" +) + +func FuzzStateStrings(f *testing.F) { + // Seed corpus: boundary values + f.Add(int32(0)) + f.Add(int32(1)) + f.Add(int32(-1)) + f.Add(int32(4)) + f.Add(int32(999)) + f.Add(int32(2147483647)) // MaxInt32 + f.Add(int32(-2147483648)) // MinInt32 + + f.Fuzz(func(t *testing.T, val int32) { + n := int(val) + + // BrowserEvent.String() + bs := avahi.BrowserEvent(n).String() + if bs == "" { + t.Errorf("BrowserEvent(%d).String() returned empty", n) + } + + // ClientState.String() + cs := avahi.ClientState(n).String() + if cs == "" { + t.Errorf("ClientState(%d).String() returned empty", n) + } + + // EntryGroupState.String() + es := avahi.EntryGroupState(n).String() + if es == "" { + t.Errorf("EntryGroupState(%d).String() returned empty", n) + } + + // ResolverEvent.String() + rs := avahi.ResolverEvent(n).String() + if rs == "" { + t.Errorf("ResolverEvent(%d).String() returned empty", n) + } + }) +} diff --git a/projects/go-avahi/oss_fuzz_build.sh b/projects/go-avahi/oss_fuzz_build.sh index 020301d..cd74ffc 100755 --- a/projects/go-avahi/oss_fuzz_build.sh +++ b/projects/go-avahi/oss_fuzz_build.sh @@ -15,16 +15,36 @@ # ################################################################################ -# Copy fuzzer source into the target library tree -mkdir -p $SRC/go-avahi/fuzzer -cp $SRC/fuzzing/projects/go-avahi/fuzzer/fuzz_domain.go $SRC/go-avahi/fuzzer/ - -# Package seed corpus +# Package seed corpus — domain normalize mkdir -p $WORK/domain_seed_corpus cp $SRC/fuzzing/projects/go-avahi/seeds/domain_seed_corpus/* $WORK/domain_seed_corpus/ cd $WORK zip -r $OUT/fuzz_domain_normalize_seed_corpus.zip domain_seed_corpus/ +# Package seed corpus — domain round-trip +mkdir -p $WORK/roundtrip_seed_corpus +cp $SRC/fuzzing/projects/go-avahi/seeds/roundtrip_seed_corpus/* $WORK/roundtrip_seed_corpus/ +zip -r $OUT/fuzz_domain_roundtrip_seed_corpus.zip roundtrip_seed_corpus/ + +# Package seed corpus — service name +mkdir -p $WORK/service_name_seed_corpus +cp $SRC/fuzzing/projects/go-avahi/seeds/service_name_seed_corpus/* $WORK/service_name_seed_corpus/ +zip -r $OUT/fuzz_service_name_seed_corpus.zip service_name_seed_corpus/ + +# Package seed corpus — state strings +mkdir -p $WORK/state_strings_seed_corpus +cp $SRC/fuzzing/projects/go-avahi/seeds/state_strings_seed_corpus/* $WORK/state_strings_seed_corpus/ +zip -r $OUT/fuzz_state_strings_seed_corpus.zip state_strings_seed_corpus/ + +# Standard build environment: the library is at /src/go-avahi +# We clean the fuzzer directory first to ensure a fresh start +rm -rf $SRC/go-avahi/fuzzer +mkdir -p $SRC/go-avahi/fuzzer +cp $SRC/fuzzing/projects/go-avahi/fuzzer/fuzz_domain.go $SRC/go-avahi/fuzzer/ +cp $SRC/fuzzing/projects/go-avahi/fuzzer/fuzz_domain_roundtrip.go $SRC/go-avahi/fuzzer/ +cp $SRC/fuzzing/projects/go-avahi/fuzzer/fuzz_service_name.go $SRC/go-avahi/fuzzer/ +cp $SRC/fuzzing/projects/go-avahi/fuzzer/fuzz_state_strings.go $SRC/go-avahi/fuzzer/ + # CGo environment: use pkg-config for architecture-agnostic library resolution export CGO_ENABLED=1 export CGO_CFLAGS="-D_REENTRANT" @@ -34,6 +54,17 @@ export CGO_LDFLAGS="$(pkg-config --libs avahi-client) -lpthread -lresolv" # clang++ link step can resolve the C symbols from the .a archive. export CXXFLAGS="${CXXFLAGS:-} $(pkg-config --libs avahi-client) -lpthread -lresolv" +# Copy required shared libraries to $OUT for the runner container +# We use ldconfig to find the exact paths. +for lib in libavahi-client.so.3 libavahi-common.so.3 libdbus-1.so.3 \ + libsystemd.so.0 libgcrypt.so.20 libgpg-error.so.0 \ + liblzma.so.5 liblz4.so.1 libcap.so.2 libz.so.1; do + LIB_PATH=$(ldconfig -p | grep -m 1 " => .*"$lib | awk '{print $4}') + if [ -n "$LIB_PATH" ]; then + cp "$LIB_PATH" "$OUT/" + fi +done + # Build dependencies and fuzzers cd $SRC/go-avahi go mod tidy @@ -41,3 +72,13 @@ go install github.com/AdamKorcz/go-118-fuzz-build@latest go get github.com/AdamKorcz/go-118-fuzz-build/testing compile_native_go_fuzzer ./fuzzer FuzzDomainNormalize fuzz_domain_normalize +compile_native_go_fuzzer ./fuzzer FuzzDomainRoundTrip fuzz_domain_roundtrip +compile_native_go_fuzzer ./fuzzer FuzzServiceName fuzz_service_name +compile_native_go_fuzzer ./fuzzer FuzzStateStrings fuzz_state_strings + +# RPATH fix: use patchelf to ensure $ORIGIN is set for all binaries +for fuzzer in fuzz_domain_normalize fuzz_domain_roundtrip fuzz_service_name fuzz_state_strings; do + if [ -f "$OUT/$fuzzer" ]; then + patchelf --set-rpath '$ORIGIN' "$OUT/$fuzzer" + fi +done diff --git a/projects/go-avahi/seeds/roundtrip_seed_corpus/deep_nested b/projects/go-avahi/seeds/roundtrip_seed_corpus/deep_nested new file mode 100644 index 0000000..f6724cf --- /dev/null +++ b/projects/go-avahi/seeds/roundtrip_seed_corpus/deep_nested @@ -0,0 +1 @@ +a.b.c.d.e.f.g.h.i.j.local diff --git a/projects/go-avahi/seeds/roundtrip_seed_corpus/escaped_backslash b/projects/go-avahi/seeds/roundtrip_seed_corpus/escaped_backslash new file mode 100644 index 0000000..c1fdaa6 --- /dev/null +++ b/projects/go-avahi/seeds/roundtrip_seed_corpus/escaped_backslash @@ -0,0 +1 @@ +My\\Service.example.com diff --git a/projects/go-avahi/seeds/roundtrip_seed_corpus/escaped_dot b/projects/go-avahi/seeds/roundtrip_seed_corpus/escaped_dot new file mode 100644 index 0000000..669e4ef --- /dev/null +++ b/projects/go-avahi/seeds/roundtrip_seed_corpus/escaped_dot @@ -0,0 +1 @@ +My\.Service.example.com diff --git a/projects/go-avahi/seeds/roundtrip_seed_corpus/simple_domain b/projects/go-avahi/seeds/roundtrip_seed_corpus/simple_domain new file mode 100644 index 0000000..de54ac6 --- /dev/null +++ b/projects/go-avahi/seeds/roundtrip_seed_corpus/simple_domain @@ -0,0 +1 @@ +example.com diff --git a/projects/go-avahi/seeds/roundtrip_seed_corpus/unicode_domain b/projects/go-avahi/seeds/roundtrip_seed_corpus/unicode_domain new file mode 100644 index 0000000..5136f5a --- /dev/null +++ b/projects/go-avahi/seeds/roundtrip_seed_corpus/unicode_domain @@ -0,0 +1 @@ +привет.example.com diff --git a/projects/go-avahi/seeds/service_name_seed_corpus/escaped_service b/projects/go-avahi/seeds/service_name_seed_corpus/escaped_service new file mode 100644 index 0000000..c8ab2c9 --- /dev/null +++ b/projects/go-avahi/seeds/service_name_seed_corpus/escaped_service @@ -0,0 +1 @@ +My\.Printer._ipp._tcp.example.com diff --git a/projects/go-avahi/seeds/service_name_seed_corpus/full_service_name b/projects/go-avahi/seeds/service_name_seed_corpus/full_service_name new file mode 100644 index 0000000..719868a --- /dev/null +++ b/projects/go-avahi/seeds/service_name_seed_corpus/full_service_name @@ -0,0 +1 @@ +Kyocera ECOSYS M2040dn._ipp._tcp.local diff --git a/projects/go-avahi/seeds/service_name_seed_corpus/ipps_service b/projects/go-avahi/seeds/service_name_seed_corpus/ipps_service new file mode 100644 index 0000000..86bff4f --- /dev/null +++ b/projects/go-avahi/seeds/service_name_seed_corpus/ipps_service @@ -0,0 +1 @@ +HP LaserJet._ipps._tcp.local diff --git a/projects/go-avahi/seeds/service_name_seed_corpus/subtype_service b/projects/go-avahi/seeds/service_name_seed_corpus/subtype_service new file mode 100644 index 0000000..d61946c --- /dev/null +++ b/projects/go-avahi/seeds/service_name_seed_corpus/subtype_service @@ -0,0 +1 @@ +Test Device._subtype._ipp._tcp.local diff --git a/projects/go-avahi/seeds/state_strings_seed_corpus/neg_one b/projects/go-avahi/seeds/state_strings_seed_corpus/neg_one new file mode 100644 index 0000000..7bde864 --- /dev/null +++ b/projects/go-avahi/seeds/state_strings_seed_corpus/neg_one @@ -0,0 +1 @@ +ÿÿÿÿ \ No newline at end of file diff --git a/projects/go-avahi/seeds/state_strings_seed_corpus/one b/projects/go-avahi/seeds/state_strings_seed_corpus/one new file mode 100644 index 0000000..f66c9cf Binary files /dev/null and b/projects/go-avahi/seeds/state_strings_seed_corpus/one differ diff --git a/projects/go-avahi/seeds/state_strings_seed_corpus/val_999 b/projects/go-avahi/seeds/state_strings_seed_corpus/val_999 new file mode 100644 index 0000000..6b66b17 Binary files /dev/null and b/projects/go-avahi/seeds/state_strings_seed_corpus/val_999 differ diff --git a/projects/go-avahi/seeds/state_strings_seed_corpus/zero b/projects/go-avahi/seeds/state_strings_seed_corpus/zero new file mode 100644 index 0000000..593f470 Binary files /dev/null and b/projects/go-avahi/seeds/state_strings_seed_corpus/zero differ