From c33e1ba580d765fd394d698e686287ff62b4b500 Mon Sep 17 00:00:00 2001 From: gaoflow Date: Wed, 29 Jul 2026 08:39:50 +0200 Subject: [PATCH] Fix panics in GetCIDRFromIPRange and IntegerToIP on valid inputs GetCIDRFromIPRange assumed a 16-byte net.IP. A 4-byte (To4) or mixed-length pair panicked with "index out of range" in rangeToCIDRs, because a normalized 16-byte spanning IP was compared against the raw 4-byte input, spuriously entering the partition branch which then dereferenced an empty slice. The same length mismatch also made the initial start>end check wrongly reject valid mixed-length ranges. Normalize both IPs to 16-byte at the entry point. IntegerToIP panicked with "index out of range [-1]" when ipInt >= 2^bits, since the copy loop indexed past the destination. Bound the copy to the destination width so oversized values wrap (mod 2^bits) instead of panicking. Both are reachable through the library's own API: net.ParseCIDR and IntegerToIP return 4-byte IPs that feed straight back into GetCIDRFromIPRange. --- cidr.go | 7 ++++- cidr_test.go | 24 +++++++++++++++++ ip.go | 7 +++++ ip_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/cidr.go b/cidr.go index 0aeec80c..d1da4fcb 100644 --- a/cidr.go +++ b/cidr.go @@ -340,7 +340,12 @@ func IPToInteger(ip net.IP) (*big.Int, int, error) { func IntegerToIP(ipInt *big.Int, bits int) net.IP { ipBytes := ipInt.Bytes() ret := make([]byte, bits/8) //nolint - for i := 1; i <= len(ipBytes); i++ { + // copy the low-order bytes; ipInt >= 2^bits wraps (mod 2^bits) instead of panicking + n := len(ipBytes) + if n > len(ret) { + n = len(ret) + } + for i := 1; i <= n; i++ { ret[len(ret)-i] = ipBytes[len(ipBytes)-i] } return net.IP(ret) diff --git a/cidr_test.go b/cidr_test.go index 1e5e3982..e179fc8f 100644 --- a/cidr_test.go +++ b/cidr_test.go @@ -1,11 +1,35 @@ package mapcidr import ( + "math/big" "net" "reflect" "testing" ) +// IntegerToIP round-trips IPToInteger and, for ipInt >= 2^bits, wraps (mod 2^bits) +// instead of panicking with a negative index. +func TestIntegerToIPOverflow(t *testing.T) { + for _, s := range []string{"0.0.0.0", "10.0.0.1", "255.255.255.255", "192.168.1.1"} { + i, bits, err := IPToInteger(net.ParseIP(s)) + if err != nil { + t.Fatalf("IPToInteger(%s): %v", s, err) + } + if got := IntegerToIP(i, bits).String(); got != s { + t.Errorf("round-trip %s => %s", s, got) + } + } + // 2^32 wraps to 0.0.0.0; 2^32 + 0x0a000001 wraps to 10.0.0.1. + over := new(big.Int).Lsh(big.NewInt(1), 32) + if got := IntegerToIP(over, 32).String(); got != "0.0.0.0" { + t.Errorf("overflow 2^32 => %s, want 0.0.0.0", got) + } + over.Add(over, big.NewInt(0x0a000001)) + if got := IntegerToIP(over, 32).String(); got != "10.0.0.1" { + t.Errorf("overflow 2^32+10.0.0.1 => %s, want 10.0.0.1", got) + } +} + func TestSplitIPNetIntoN(t *testing.T) { tests := []struct { name string diff --git a/ip.go b/ip.go index 68d8ccee..76dd63e1 100644 --- a/ip.go +++ b/ip.go @@ -1134,6 +1134,13 @@ The intent here is to get the CIDR range from the IP range. This function will return the sorted list of CIDR ranges. */ func GetCIDRFromIPRange(firstIP, lastIP net.IP) ([]*net.IPNet, error) { + // net.IP is 4- or 16-byte; normalise both to 16-byte so the range math below + // never depends on the input representation. + first16, last16 := firstIP.To16(), lastIP.To16() + if first16 == nil || last16 == nil { + return nil, fmt.Errorf("invalid IP address in range %s-%s", firstIP, lastIP) + } + firstIP, lastIP = first16, last16 // check if range is valid or not if bytes.Compare(firstIP, lastIP) > 0 { return nil, fmt.Errorf("start IP:%s must be less than End IP:%s", firstIP, lastIP) diff --git a/ip_test.go b/ip_test.go index d12bd7df..499abdcc 100644 --- a/ip_test.go +++ b/ip_test.go @@ -101,6 +101,82 @@ func TestRangeToCIDRs(t *testing.T) { } } +// cidrStrings runs GetCIDRFromIPRange and returns the CIDR list as strings. +func cidrStrings(t *testing.T, first, last net.IP) []string { + t.Helper() + got, err := GetCIDRFromIPRange(first, last) + require.NoError(t, err) + var out []string + for _, n := range got { + out = append(out, n.String()) + } + return out +} + +// GetCIDRFromIPRange must accept any legitimate net.IP byte-length. Previously a +// 4-byte (To4) or mixed-length pair panicked, and a mixed-length valid range was +// wrongly rejected by the initial start>end check. +func TestGetCIDRFromIPRangeByteLength(t *testing.T) { + first16 := net.ParseIP("10.0.0.1") + last16 := net.ParseIP("10.0.0.9") + want := cidrStrings(t, first16, last16) + require.Equal(t, []string{"10.0.0.1/32", "10.0.0.2/31", "10.0.0.4/30", "10.0.0.8/31"}, want) + + cases := []struct { + name string + first, last net.IP + }{ + {"both4byte", first16.To4(), last16.To4()}, + {"first16last4", first16, last16.To4()}, + {"first4last16", first16.To4(), last16}, + {"integerToIPOutput", intToIPHelper(t, "10.0.0.1"), intToIPHelper(t, "10.0.0.9")}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, want, cidrStrings(t, tc.first, tc.last)) + }) + } +} + +func intToIPHelper(t *testing.T, s string) net.IP { + t.Helper() + i, bits, err := IPToInteger(net.ParseIP(s)) + require.NoError(t, err) + return IntegerToIP(i, bits) // 4-byte form +} + +// tiling parity: 4-byte, 16-byte and mixed inputs must yield identical, correct +// CIDR sets (aligned, contiguous, non-overlapping, exact cover of [start,end]). +func TestGetCIDRFromIPRangeTilingParity(t *testing.T) { + base := new(big.Int).SetBytes(net.ParseIP("172.16.0.0").To4()) + for k := 0; k < 4000; k++ { + a := int64((k * 2654435761) % (1 << 20)) + b := a + int64((k*40503)%(1<<12)) + s4 := IntegerToIP(new(big.Int).Add(base, big.NewInt(a)), 32) + e4 := IntegerToIP(new(big.Int).Add(base, big.NewInt(b)), 32) + got16 := cidrStrings(t, s4.To16(), e4.To16()) + require.Equal(t, got16, cidrStrings(t, s4, e4), "4-byte parity") + require.Equal(t, got16, cidrStrings(t, s4.To16(), e4), "mixed parity") + assertTiling(t, s4, e4, got16) + } +} + +func assertTiling(t *testing.T, start, end net.IP, cidrs []string) { + t.Helper() + lo := new(big.Int).SetBytes(start.To4()) + end4 := new(big.Int).SetBytes(end.To4()) + for _, c := range cidrs { + _, n, err := net.ParseCIDR(c) + require.NoError(t, err) + ones, size := n.Mask.Size() + netLo := new(big.Int).SetBytes(n.IP.To4()) + require.Equal(t, 0, lo.Cmp(netLo), "gap/overlap or misalignment at %s", c) + blk := new(big.Int).Lsh(big.NewInt(1), uint(size-ones)) + lo = new(big.Int).Add(netLo, blk) + } + require.Equal(t, 0, lo.Cmp(new(big.Int).Add(end4, big.NewInt(1))), "range not covered exactly") +} + func TestFindSmallestIPRange(t *testing.T) { // Input IP addresses ips := []string{