From e4287c1767aceb5c4496a67894c434400174c260 Mon Sep 17 00:00:00 2001 From: Rishav Tarway Date: Tue, 10 Mar 2026 14:00:32 +0530 Subject: [PATCH] feat(go-avahi): implement comprehensive stateful and DNS decoding fuzzing suite This update expands the go-avahi fuzzing coverage with the following technical enhancements: - Stateful Lifecycle Fuzzing: Added harnesses for EntryGroup and ServiceBrowser utilizing live avahi-daemon interaction. - Pure Go Logic: Implemented targeted DNS resource record decoding fuzzers (A, AAAA, TXT). - Architectural Alignment: Standardized headers and migrated to external seed corpus management to match OpenPrinting standards. - Build Portability: Enhanced RPATH patching in build scripts for robust container execution. --- .../go-avahi/fuzzer/fuzz_client_lifecycle.go | 78 +++++++++++++++++++ projects/go-avahi/fuzzer/fuzz_dns.go | 27 +++++++ projects/go-avahi/fuzzer/fuzz_entry_group.go | 64 +++++++++++++++ .../go-avahi/fuzzer/fuzz_service_browser.go | 54 +++++++++++++ .../go-avahi/fuzzer/fuzz_state_strings.go | 15 ++-- projects/go-avahi/fuzzer/fuzz_string_array.go | 73 +++++++++++++++++ projects/go-avahi/oss_fuzz_build.sh | 24 +++++- .../client_lifecycle_seed_corpus/four_iter | 1 + .../client_lifecycle_seed_corpus/max_iter | 1 + .../client_lifecycle_seed_corpus/one_iter | 1 + .../string_array_seed_corpus/txt_ipp_record | 3 + .../string_array_seed_corpus/txt_key_value | 1 + .../txt_printer_attrs | 3 + 13 files changed, 337 insertions(+), 8 deletions(-) create mode 100644 projects/go-avahi/fuzzer/fuzz_client_lifecycle.go create mode 100644 projects/go-avahi/fuzzer/fuzz_dns.go create mode 100644 projects/go-avahi/fuzzer/fuzz_entry_group.go create mode 100644 projects/go-avahi/fuzzer/fuzz_service_browser.go create mode 100644 projects/go-avahi/fuzzer/fuzz_string_array.go create mode 100644 projects/go-avahi/seeds/client_lifecycle_seed_corpus/four_iter create mode 100644 projects/go-avahi/seeds/client_lifecycle_seed_corpus/max_iter create mode 100644 projects/go-avahi/seeds/client_lifecycle_seed_corpus/one_iter create mode 100644 projects/go-avahi/seeds/string_array_seed_corpus/txt_ipp_record create mode 100644 projects/go-avahi/seeds/string_array_seed_corpus/txt_key_value create mode 100644 projects/go-avahi/seeds/string_array_seed_corpus/txt_printer_attrs diff --git a/projects/go-avahi/fuzzer/fuzz_client_lifecycle.go b/projects/go-avahi/fuzzer/fuzz_client_lifecycle.go new file mode 100644 index 0000000..748ccd7 --- /dev/null +++ b/projects/go-avahi/fuzzer/fuzz_client_lifecycle.go @@ -0,0 +1,78 @@ +/* + * Fuzz target for go-avahi's Client lifecycle. + * + * Tests rapid creation and tear-down of Clients to detect race + * conditions, resource leaks, and panics in the CGo initialization + * path. Requires a running avahi-daemon; skips if unavailable. + * + * Also exercises GetVersionString, GetHostName, GetDomainName, and + * GetHostFQDN with fuzz-driven repetition to stress the threaded poll + * lock/unlock cycle. + */ + +package fuzzer + +import ( + "context" + "testing" + "time" + + avahi "github.com/OpenPrinting/go-avahi" +) + +func FuzzClientLifecycle(f *testing.F) { + f.Fuzz(func(t *testing.T, iterations uint8) { + // Clamp to a sane range to avoid exhausting file descriptors + if iterations == 0 { + iterations = 1 + } + if iterations > 16 { + iterations = 16 + } + + for i := 0; i < int(iterations); i++ { + clnt, err := avahi.NewClient(0) + if err != nil { + // avahi-daemon not running; skip gracefully + t.Skip("avahi-daemon not available") + return + } + + // Give the client a short window to connect and + // report its initial state via the event channel. + ctx, cancel := context.WithTimeout( + context.Background(), 200*time.Millisecond) + + // Drain the first event (connecting / running) + clnt.Get(ctx) //nolint:errcheck + cancel() + + // Exercise the query methods that call into CGo + // under the threaded poll lock, verifying no panics + // and no empty return values. + version := clnt.GetVersionString() + if version == "" { + t.Error("GetVersionString returned empty string") + } + + host := clnt.GetHostName() + if host == "" { + t.Error("GetHostName returned empty string") + } + + domain := clnt.GetDomainName() + if domain == "" { + t.Error("GetDomainName returned empty string") + } + + fqdn := clnt.GetHostFQDN() + if fqdn == "" { + t.Error("GetHostFQDN returned empty string") + } + + // Close must be idempotent — call twice to verify + clnt.Close() + clnt.Close() + } + }) +} diff --git a/projects/go-avahi/fuzzer/fuzz_dns.go b/projects/go-avahi/fuzzer/fuzz_dns.go new file mode 100644 index 0000000..d7609d0 --- /dev/null +++ b/projects/go-avahi/fuzzer/fuzz_dns.go @@ -0,0 +1,27 @@ +//go:build linux || freebsd + +package fuzzer + +import ( + "testing" + + avahi "github.com/OpenPrinting/go-avahi" +) + +func FuzzDecodeDNSA(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + _ = avahi.DNSDecodeA(data) + }) +} + +func FuzzDNSAAAA(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + _ = avahi.DNSDecodeAAAA(data) + }) +} + +func FuzzDNSTXT(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + _ = avahi.DNSDecodeTXT(data) + }) +} diff --git a/projects/go-avahi/fuzzer/fuzz_entry_group.go b/projects/go-avahi/fuzzer/fuzz_entry_group.go new file mode 100644 index 0000000..6b06601 --- /dev/null +++ b/projects/go-avahi/fuzzer/fuzz_entry_group.go @@ -0,0 +1,64 @@ +// CGo binding for Avahi +// +// Copyright (C) 2024 and up by Alexander Pevzner (pzz@apevzner.com) +// See LICENSE for license terms and conditions +// +// Fuzz target for go-avahi's EntryGroup lifecycle +// +//go:build linux || freebsd + +package fuzzer + +import ( + "context" + "testing" + "time" + + avahi "github.com/OpenPrinting/go-avahi" +) + +func FuzzEntryGroupLifecycle(f *testing.F) { + f.Fuzz(func(t *testing.T, count uint8, svcName string, svcType string) { + if count == 0 { + count = 1 + } + if count > 5 { + count = 5 + } + + clnt, err := avahi.NewClient(0) + if err != nil { + t.Skip("avahi-daemon not available") + return + } + defer clnt.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + clnt.Get(ctx) //nolint:errcheck + cancel() + + egrp, err := avahi.NewEntryGroup(clnt) + if err != nil { + return + } + defer egrp.Close() + + for i := 0; i < int(count); i++ { + _ = egrp.AddService(&avahi.EntryGroupService{ + IfIdx: avahi.IfIndexUnspec, + Proto: avahi.ProtocolUnspec, + InstanceName: svcName, + SvcType: svcType, + Domain: "local", + Port: 8080, + Txt: []string{"key=value"}, + }, 0) + + _ = egrp.Commit() + _ = egrp.Reset() + + // Optional intermediate commits + _ = egrp.Commit() + } + }) +} diff --git a/projects/go-avahi/fuzzer/fuzz_service_browser.go b/projects/go-avahi/fuzzer/fuzz_service_browser.go new file mode 100644 index 0000000..408369d --- /dev/null +++ b/projects/go-avahi/fuzzer/fuzz_service_browser.go @@ -0,0 +1,54 @@ +// CGo binding for Avahi +// +// Copyright (C) 2024 and up by Alexander Pevzner (pzz@apevzner.com) +// See LICENSE for license terms and conditions +// +// Fuzz target for go-avahi's EntryGroup lifecycle +// +//go:build linux || freebsd + +package fuzzer + +import ( + "context" + "testing" + "time" + + avahi "github.com/OpenPrinting/go-avahi" +) + +func FuzzServiceBrowserLifecycle(f *testing.F) { + f.Fuzz(func(t *testing.T, count uint8, svcType string, domain string) { + if count == 0 { + count = 1 + } + if count > 5 { + count = 5 + } + + clnt, err := avahi.NewClient(0) + if err != nil { + t.Skip("avahi-daemon not available") + return + } + defer clnt.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + clnt.Get(ctx) //nolint:errcheck + cancel() + + for i := 0; i < int(count); i++ { + browser, err := avahi.NewServiceBrowser(clnt, avahi.IfIndexUnspec, avahi.ProtocolUnspec, svcType, domain, 0) + if err != nil { + continue + } + + // Optional: try to fetch an event rapidly before tearing down + ctx2, cancel2 := context.WithTimeout(context.Background(), 10*time.Millisecond) + browser.Get(ctx2) //nolint:errcheck + cancel2() + + browser.Close() + } + }) +} diff --git a/projects/go-avahi/fuzzer/fuzz_state_strings.go b/projects/go-avahi/fuzzer/fuzz_state_strings.go index 33c6a0a..a9f69cb 100644 --- a/projects/go-avahi/fuzzer/fuzz_state_strings.go +++ b/projects/go-avahi/fuzzer/fuzz_state_strings.go @@ -1,10 +1,11 @@ -/* - * 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. - */ +// CGo binding for Avahi +// +// Copyright (C) 2024 and up by Alexander Pevzner (pzz@apevzner.com) +// See LICENSE for license terms and conditions +// +// Fuzz target for go-avahi's Domain round-trip consistency +// +//go:build linux || freebsd package fuzzer diff --git a/projects/go-avahi/fuzzer/fuzz_string_array.go b/projects/go-avahi/fuzzer/fuzz_string_array.go new file mode 100644 index 0000000..ef23778 --- /dev/null +++ b/projects/go-avahi/fuzzer/fuzz_string_array.go @@ -0,0 +1,73 @@ +// CGo binding for Avahi +// +// Copyright (C) 2024 and up by Alexander Pevzner (pzz@apevzner.com) +// See LICENSE for license terms and conditions +// +// Fuzz target for go-avahi's string list CGo conversion path +// +//go:build linux || freebsd + +package fuzzer + +import ( + "strings" + "testing" + "unicode/utf8" + + avahi "github.com/OpenPrinting/go-avahi" +) + +func FuzzStringArray(f *testing.F) { + f.Fuzz(func(t *testing.T, data string) { + // Only valid UTF-8: Avahi TXT records are byte strings but + // the Go layer uses string, and invalid UTF-8 would not + // represent real-world TXT record inputs. + if !utf8.ValidString(data) { + return + } + + // Split fuzz data into TXT record entries using newline as + // delimiter, simulating key=value pairs in mDNS TXT records. + raw := strings.Split(data, "\n") + + var txt []string + for _, e := range raw { + if len(e) > 0 && len(e) <= 255 { + // TXT record strings are limited to 255 bytes + // per the DNS spec; enforce the limit. + txt = append(txt, e) + } + } + + if len(txt) == 0 { + return + } + + // Create a client to exercise the full EntryGroup path that + // calls makeAvahiStringList internally. Skip if no daemon. + clnt, err := avahi.NewClient(0) + if err != nil { + t.Skip("avahi-daemon not available") + return + } + defer clnt.Close() + + egrp, err := avahi.NewEntryGroup(clnt) + if err != nil { + return + } + defer egrp.Close() + + // AddService calls makeAvahiStringList on the txt slice. + // We do not assert success — we assert no panic and no crash. + _ = egrp.AddService(&avahi.EntryGroupService{ + IfIdx: avahi.IfIndexUnspec, + Proto: avahi.ProtocolUnspec, + InstanceName: "fuzz-test", + SvcType: "_fuzz._tcp", + Domain: "", + Port: 9999, + Txt: txt, + }, 0) + }) +} diff --git a/projects/go-avahi/oss_fuzz_build.sh b/projects/go-avahi/oss_fuzz_build.sh index 7771221..2211d1e 100755 --- a/projects/go-avahi/oss_fuzz_build.sh +++ b/projects/go-avahi/oss_fuzz_build.sh @@ -21,6 +21,16 @@ 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/ +# Package seed corpus — string array +mkdir -p $WORK/string_array_seed_corpus +cp $SRC/fuzzing/projects/go-avahi/seeds/string_array_seed_corpus/* $WORK/string_array_seed_corpus/ +zip -r $OUT/fuzz_string_array_seed_corpus.zip string_array_seed_corpus/ + +# Package seed corpus — client lifecycle +mkdir -p $WORK/client_lifecycle_seed_corpus +cp $SRC/fuzzing/projects/go-avahi/seeds/client_lifecycle_seed_corpus/* $WORK/client_lifecycle_seed_corpus/ +zip -r $OUT/fuzz_client_lifecycle_seed_corpus.zip client_lifecycle_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 @@ -29,6 +39,11 @@ 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/ +cp $SRC/fuzzing/projects/go-avahi/fuzzer/fuzz_string_array.go $SRC/go-avahi/fuzzer/ +cp $SRC/fuzzing/projects/go-avahi/fuzzer/fuzz_client_lifecycle.go $SRC/go-avahi/fuzzer/ +cp $SRC/fuzzing/projects/go-avahi/fuzzer/fuzz_dns.go $SRC/go-avahi/fuzzer/ +cp $SRC/fuzzing/projects/go-avahi/fuzzer/fuzz_entry_group.go $SRC/go-avahi/fuzzer/ +cp $SRC/fuzzing/projects/go-avahi/fuzzer/fuzz_service_browser.go $SRC/go-avahi/fuzzer/ # CGo environment: use pkg-config for architecture-agnostic library resolution export CGO_ENABLED=1 @@ -60,9 +75,16 @@ 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 +compile_native_go_fuzzer ./fuzzer FuzzStringArray fuzz_string_array +compile_native_go_fuzzer ./fuzzer FuzzClientLifecycle fuzz_client_lifecycle +compile_native_go_fuzzer ./fuzzer FuzzDecodeDNSA fuzz_dns_decode_a +compile_native_go_fuzzer ./fuzzer FuzzDNSAAAA fuzz_dns_decode_aaaa +compile_native_go_fuzzer ./fuzzer FuzzDNSTXT fuzz_dns_decode_txt +compile_native_go_fuzzer ./fuzzer FuzzEntryGroupLifecycle fuzz_entry_group +compile_native_go_fuzzer ./fuzzer FuzzServiceBrowserLifecycle fuzz_service_browser # 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 +for fuzzer in fuzz_domain_normalize fuzz_domain_roundtrip fuzz_service_name fuzz_state_strings fuzz_string_array fuzz_client_lifecycle fuzz_dns_decode_a fuzz_dns_decode_aaaa fuzz_dns_decode_txt fuzz_entry_group fuzz_service_browser; do if [ -f "$OUT/$fuzzer" ]; then patchelf --set-rpath '$ORIGIN' "$OUT/$fuzzer" fi diff --git a/projects/go-avahi/seeds/client_lifecycle_seed_corpus/four_iter b/projects/go-avahi/seeds/client_lifecycle_seed_corpus/four_iter new file mode 100644 index 0000000..45a8ca0 --- /dev/null +++ b/projects/go-avahi/seeds/client_lifecycle_seed_corpus/four_iter @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/go-avahi/seeds/client_lifecycle_seed_corpus/max_iter b/projects/go-avahi/seeds/client_lifecycle_seed_corpus/max_iter new file mode 100644 index 0000000..31f442a --- /dev/null +++ b/projects/go-avahi/seeds/client_lifecycle_seed_corpus/max_iter @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/go-avahi/seeds/client_lifecycle_seed_corpus/one_iter b/projects/go-avahi/seeds/client_lifecycle_seed_corpus/one_iter new file mode 100644 index 0000000..6b2aaa7 --- /dev/null +++ b/projects/go-avahi/seeds/client_lifecycle_seed_corpus/one_iter @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/go-avahi/seeds/string_array_seed_corpus/txt_ipp_record b/projects/go-avahi/seeds/string_array_seed_corpus/txt_ipp_record new file mode 100644 index 0000000..5c3c3e4 --- /dev/null +++ b/projects/go-avahi/seeds/string_array_seed_corpus/txt_ipp_record @@ -0,0 +1,3 @@ +txtvers=1 +pdl=application/pdf,image/jpeg +printer-state=3 \ No newline at end of file diff --git a/projects/go-avahi/seeds/string_array_seed_corpus/txt_key_value b/projects/go-avahi/seeds/string_array_seed_corpus/txt_key_value new file mode 100644 index 0000000..4b10332 --- /dev/null +++ b/projects/go-avahi/seeds/string_array_seed_corpus/txt_key_value @@ -0,0 +1 @@ +key=value \ No newline at end of file diff --git a/projects/go-avahi/seeds/string_array_seed_corpus/txt_printer_attrs b/projects/go-avahi/seeds/string_array_seed_corpus/txt_printer_attrs new file mode 100644 index 0000000..47d7e4d --- /dev/null +++ b/projects/go-avahi/seeds/string_array_seed_corpus/txt_printer_attrs @@ -0,0 +1,3 @@ +adminurl=http://localhost:631/printers/test +color=T +copies=T \ No newline at end of file