package agent import ( "testing" ) // FuzzParseAnnotations explores the joint space of {ipv6, ipv4, cidr6, cidr4, // ip-algo, anycast} annotations with random byte strings. Every recognised // key is exercised by deriving a deterministic input map from the fuzzed // bytes; this gives the fuzzer reach into all parser branches at once. // // Properties checked: // // 1. The parser never panics on any input. // 2. On nil-error return, the result satisfies the design-doc invariant // that at least one of WantV6 / WantV4 is true (a pod always has at // least one address). // 3. Anycast IPs and IPAlgo fields are non-nil/empty only when the // annotation was supplied; never spontaneously populated. // // Seed corpus covers known edge cases the spec must handle. func FuzzParseAnnotations(f *testing.F) { // Seeds: each entry is six strings — the literal raw values for the // six parsed keys. Empty string for "key absent". type seed struct { ipv6, ipv4, cidr6, cidr4, ipAlgo, anycast string } seeds := []seed{ {}, {ipv4: "true"}, {ipv6: "false", ipv4: "true"}, {ipv6: "TRUE"}, {ipv6: " true "}, {ipv6: "yes"}, // invalid → expect error {ipv4: "1"}, // invalid {cidr6: ""}, // invalid (empty after split) {cidr6: ","}, // invalid (empty after trim) {cidr6: "2602:817:3000:f001::/64"}, // valid single {cidr6: "2602:817:3000:f001::/64,"}, // trailing comma {cidr6: " 2602:817:3000:f001::/64 "}, // surrounding whitespace {cidr6: "2602:817:3000:f001::/64, 2602:817:3000:f002::/64"}, {cidr6: "10.0.0.0/8"}, // family mismatch {cidr4: "172.25.210.0/24"}, // valid {cidr4: "172.25.210.0/24,172.25.211.0/24"}, // multiple {cidr4: "2602:817::/32"}, // family mismatch {ipAlgo: "namespace,pod,image"}, {ipAlgo: "namespace, pod , image"}, // whitespace {ipAlgo: "namespace,unknown"}, // invalid {ipAlgo: ""}, // invalid (empty) {ipAlgo: ","}, // invalid {anycast: "2602:817:3000:ac::1"}, {anycast: "2602:817:3000:ac::1, 172.25.255.1"}, {anycast: "::1"}, // loopback (allowed at parse time) {anycast: "fe80::1"}, // link-local (allowed at parse time) {anycast: "::ffff:10.0.0.1"}, // v4-mapped v6 {anycast: "0.0.0.0"}, // unspecified {anycast: "definitely-not-an-ip"}, // invalid {anycast: ""}, // invalid // Embedded NUL bytes {ipv4: "true\x00"}, {cidr6: "2602:817:3000:f001::/64\x00"}, {anycast: "\x00\x00"}, // Unicode {ipv4: "trüe"}, {ipAlgo: "námespace"}, // Very long {cidr6: longString("2602:817:3000:f001::/64,", 4096)}, } for _, s := range seeds { f.Add(s.ipv6, s.ipv4, s.cidr6, s.cidr4, s.ipAlgo, s.anycast) } f.Fuzz(func(t *testing.T, ipv6, ipv4, cidr6, cidr4, ipAlgo, anycast string) { in := map[string]string{} // Treat empty as "key absent" so the seed table matches the run-time // shape; Kubernetes annotations cannot have a nil value but they CAN // be missing entirely. Empty-string-with-key is also a real case // (operator typo); add a separate seed below to cover it. if ipv6 != "" { in[annotationPrefix+annIPv6] = ipv6 } if ipv4 != "" { in[annotationPrefix+annIPv4] = ipv4 } if cidr6 != "" { in[annotationPrefix+annCIDR6] = cidr6 } if cidr4 != "" { in[annotationPrefix+annCIDR4] = cidr4 } if ipAlgo != "" { in[annotationPrefix+annIPAlgo] = ipAlgo } if anycast != "" { in[annotationPrefix+annAnycast] = anycast } got, err := ParseAnnotations(in, BuiltinFamilyDefaults()) if err != nil { return // any error is acceptable; we only require no panic } // Property: at least one family must be selected. if !got.WantV6 && !got.WantV4 { t.Fatalf("parser accepted but produced no family: in=%#v", in) } // Property: optional fields populated only when their key was set. if _, hasAlgo := in[annotationPrefix+annIPAlgo]; !hasAlgo && len(got.IPAlgo) != 0 { t.Fatalf("IPAlgo populated without annotation") } if _, hasAny := in[annotationPrefix+annAnycast]; !hasAny && len(got.Anycast) != 0 { t.Fatalf("Anycast populated without annotation") } if _, hasC6 := in[annotationPrefix+annCIDR6]; !hasC6 && len(got.CIDR6) != 0 { t.Fatalf("CIDR6 populated without annotation") } if _, hasC4 := in[annotationPrefix+annCIDR4]; !hasC4 && len(got.CIDR4) != 0 { t.Fatalf("CIDR4 populated without annotation") } }) } // FuzzParseCNIArgs requires the parser to never panic on adversarial inputs. // The parser is permissive by spec — it returns a CNIArgs with whatever it // could extract — so the only invariant is "doesn't crash". func FuzzParseCNIArgs(f *testing.F) { f.Add("") f.Add("=") f.Add(";") f.Add(";=;=;") f.Add("K8S_POD_NAMESPACE=ns;K8S_POD_NAME=p") f.Add("K8S_POD_NAMESPACE=ns;K8S_POD_NAME=p;K8S_POD_UID=abc;K8S_POD_INFRA_CONTAINER_ID=def") f.Add("=value-only") f.Add("key-only=") f.Add("\x00\x00\x00") f.Add("K8S_POD_NAMESPACE=\xff\xfe\xfd") f.Add("K8S_POD_NAME=value;K8S_POD_NAME=other") // duplicate keys: last wins // Long input f.Add(longString("K8S_POD_NAME=x;", 4096)) f.Fuzz(func(t *testing.T, in string) { _ = ParseCNIArgs(in) }) } // longString returns s repeated to total >= n bytes, useful for piling up // realistic-looking but oversized inputs. func longString(s string, n int) string { if len(s) == 0 { return "" } var b []byte for len(b) < n { b = append(b, s...) } return string(b) }