Fix potential integer math crash on PublishPort. (#1612)

- Closes #1610.
- Discovered, and originally filed as a security advisory, by:
PresidentL <131139636+liyander@users.noreply.github.com>.
- `PublishPort` currently can store invalid combinations of starting
port and range that can overflow UInt16 values when summed, crashing the
process.
- Updates `PublishPort` to validate inputs on initialization.
This commit is contained in:
J Logan
2026-05-28 11:12:04 -07:00
committed by GitHub
parent 046884df37
commit 445c90f927
3 changed files with 86 additions and 12 deletions
@@ -14,6 +14,7 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationError
import ContainerizationExtras
/// The network protocols available for port forwarding.
@@ -54,12 +55,20 @@ public struct PublishPort: Sendable, Codable {
public let count: UInt16
/// Creates a new port forwarding specification.
public init(hostAddress: IPAddress, hostPort: UInt16, containerPort: UInt16, proto: PublishProtocol, count: UInt16) {
public init(
hostAddress: IPAddress,
hostPort: UInt16,
containerPort: UInt16,
proto: PublishProtocol,
count: UInt16
) throws {
self.hostAddress = hostAddress
self.hostPort = hostPort
self.containerPort = containerPort
self.proto = proto
self.count = count
try validatePortRange(port: hostPort, count: count)
try validatePortRange(port: containerPort, count: count)
}
/// Create a configuration from the supplied Decoder, initializing missing
@@ -72,6 +81,14 @@ public struct PublishPort: Sendable, Codable {
containerPort = try container.decode(UInt16.self, forKey: .containerPort)
proto = try container.decode(PublishProtocol.self, forKey: .proto)
count = try container.decodeIfPresent(UInt16.self, forKey: .count) ?? 1
try validatePortRange(port: hostPort, count: count)
try validatePortRange(port: containerPort, count: count)
}
private func validatePortRange(port: UInt16, count: UInt16) throws {
guard count > 0, UInt16.max - port >= count - 1 else {
throw ContainerizationError(.invalidArgument, message: "invalid port and count: \(port), \(count)")
}
}
}
@@ -79,8 +96,8 @@ extension [PublishPort] {
public func hasOverlaps() -> Bool {
var hostPorts = Set<String>()
for publishPort in self {
for index in publishPort.hostPort..<(publishPort.hostPort + publishPort.count) {
let hostPortKey = "\(index)/\(publishPort.proto.rawValue)"
for offset in 0..<publishPort.count {
let hostPortKey = "\(publishPort.hostPort + offset)/\(publishPort.proto.rawValue)"
guard !hostPorts.contains(hostPortKey) else {
return true
}
@@ -701,7 +701,7 @@ public struct Parser {
throw ContainerizationError(.invalidArgument, message: "publish host and container port counts are not equal: \(hostPortText):\(containerPortText)")
}
return PublishPort(
return try PublishPort(
hostAddress: hostAddress,
hostPort: hostPortRangeStart,
containerPort: containerPortRangeStart,
@@ -24,8 +24,8 @@ struct PublishPortTests {
@Test
func testPublishPortsNonOverlapping() throws {
let ports = [
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 9000, containerPort: 8080, proto: .tcp, count: 100),
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 9100, containerPort: 8180, proto: .tcp, count: 100),
try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 9000, containerPort: 8080, proto: .tcp, count: 100),
try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 9100, containerPort: 8180, proto: .tcp, count: 100),
]
#expect(!ports.hasOverlaps())
}
@@ -33,8 +33,8 @@ struct PublishPortTests {
@Test
func testPublishPortsOverlapping() throws {
let ports = [
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 9000, containerPort: 8080, proto: .tcp, count: 101),
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 9100, containerPort: 8180, proto: .tcp, count: 100),
try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 9000, containerPort: 8080, proto: .tcp, count: 101),
try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 9100, containerPort: 8180, proto: .tcp, count: 100),
]
#expect(ports.hasOverlaps())
}
@@ -42,11 +42,68 @@ struct PublishPortTests {
@Test
func testPublishPortsSamePortDifferentProtocols() throws {
let ports = [
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 8080, containerPort: 8080, proto: .tcp, count: 1),
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 8080, containerPort: 8080, proto: .udp, count: 1),
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 1024, containerPort: 1024, proto: .tcp, count: 1025),
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 1024, containerPort: 1024, proto: .udp, count: 1025),
try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 8080, containerPort: 8080, proto: .tcp, count: 1),
try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 8080, containerPort: 8080, proto: .udp, count: 1),
try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 1024, containerPort: 1024, proto: .tcp, count: 1025),
try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 1024, containerPort: 1024, proto: .udp, count: 1025),
]
#expect(!ports.hasOverlaps())
}
@Test
func testPublishPortHostPortOverflowRejected() throws {
#expect(throws: (any Error).self) {
try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 65535, containerPort: 8080, proto: .tcp, count: 2)
}
}
@Test
func testPublishPortContainerPortOverflowRejected() throws {
#expect(throws: (any Error).self) {
try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 8080, containerPort: 65535, proto: .tcp, count: 2)
}
}
@Test
func testPublishPortZeroCountRejected() throws {
#expect(throws: (any Error).self) {
try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 8080, containerPort: 8080, proto: .tcp, count: 0)
}
}
@Test
func testPublishPortRangeEndingAtMaxValid() throws {
// hostPort 65534 + count 2 last port 65535, should be accepted
_ = try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 65534, containerPort: 8080, proto: .tcp, count: 2)
}
@Test
func testPublishPortSingleMaxPortValid() throws {
_ = try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 65535, containerPort: 8080, proto: .tcp, count: 1)
}
@Test
func testPublishPortDecodeRejectsHostPortOverflow() throws {
let json = Data(#"{"hostAddress":"0.0.0.0","hostPort":65535,"containerPort":8080,"proto":"tcp","count":2}"#.utf8)
#expect(throws: (any Error).self) {
try JSONDecoder().decode(PublishPort.self, from: json)
}
}
@Test
func testPublishPortDecodeRejectsContainerPortOverflow() throws {
let json = Data(#"{"hostAddress":"0.0.0.0","hostPort":8080,"containerPort":65535,"proto":"tcp","count":2}"#.utf8)
#expect(throws: (any Error).self) {
try JSONDecoder().decode(PublishPort.self, from: json)
}
}
@Test
func testHasOverlapsAtMaxPort() throws {
let ports = [
try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 65534, containerPort: 8080, proto: .tcp, count: 2),
try PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 65534, containerPort: 9090, proto: .tcp, count: 1),
]
#expect(ports.hasOverlaps())
}
}