Skip to content

[Bug]: CIDR.contains excludes members of a block written from a host address #826

Description

@vyncint

I have done the following

  • I have searched the existing issues
  • I reproduced the issue against main at ff44a5b683c80fceab875dba8a20ed24d7648c07

Steps to reproduce

CIDR keeps the address exactly as parsed, so a block written from a host address keeps its host bits set. Containment then compares that unmasked value against the masked probe.

Add this to Tests/ContainerizationExtrasTests/TestCIDR.swift and run swift test --filter TestCIDR:

@Test func reproduce() throws {
    let block = try CIDR("192.168.1.100/24")
    #expect(block.contains(try IPAddress("192.168.1.100")))  // fails
    #expect(block.contains(try IPAddress("192.168.1.0")))    // fails

    // the type it wraps gets this right
    let concrete = try CIDRv4("192.168.1.100/24")
    #expect(concrete.contains(try IPv4Address("192.168.1.100")))  // passes
}

Current behavior

The wrapper disagrees with the type it wraps, in both families:

expression CIDR CIDRv4 / CIDRv6
192.168.1.100/24 contains 192.168.1.100 false true
192.168.1.100/24 contains 192.168.1.0 false true
10.1.2.3/16 contains 10.1.99.99 false true
2001:db8::1234/64 contains 2001:db8::1 false true

CIDR.contains compares an unmasked stored address (Sources/ContainerizationExtras/CIDR.swift:116 and :118):

return network.value == (ip.value & prefix.prefixMask32)

whereas CIDRv4.contains masks both sides:

(address.value & prefix.prefixMask32) == (ip.value & prefix.prefixMask32)

A block already written in network form (192.168.1.0/24) is unaffected, which is why the current tests pass: every containment case in TestCIDR.swift uses a network address.

There is a second defect in the same accessors. lower drops the IPv6 zone while upper keeps it (CIDR.swift:94 vs :105, and CIDRv6.swift:81 vs :87):

2001:db8::5%lo0/126  ->  lower = 2001:db8::4       (zone lost)
                         upper = 2001:db8::7%lo0
contains(lower) = false

Because contains compares zones, the missing zone on its own is enough to place the bound outside its own block.

Expected behavior

CIDR should agree with CIDRv4 and CIDRv6 on both containment and bounds: mask the stored address before comparing, and carry the address's zone into lower the way upper already does.

Environment

macOS 26.6 (25G72), Xcode 26.6, Swift 6.3, arm64
main @ ff44a5b683c80fceab875dba8a20ed24d7648c07

Relevant log output

✘ Expectation failed: block.contains(try IPAddress(ip))
    cidr → "192.168.1.100/24", ip → "192.168.1.100"
✘ Expectation failed: (wrapper.contains(.v4(ip)) → false) == (concrete.contains(ip) → true)
    cidr → "192.168.1.100/24"
✘ Expectation failed: (block → 2001:db8::5%lo0/126).contains(block.lower → 2001:db8::4)

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions