diff --git a/CHANGELOG.md b/CHANGELOG.md index aa14506..91ac6a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ Cutting that release is tracked in ### Fixed +- Linux DevTools-pipe framing now tracks its scan cursor and amortizes buffer + compaction, avoiding quadratic work for large screenshot responses. - Phase 1 hardening now bounds Chromium teardown after `SIGKILL`, uses libc's peer-credential constant, deterministically caps diagnostic headers, drops malformed CDP header values, atomically finalizes artifacts without diff --git a/apps/headless/LinuxHost/CDP.swift b/apps/headless/LinuxHost/CDP.swift index 544486d..12d7f66 100644 --- a/apps/headless/LinuxHost/CDP.swift +++ b/apps/headless/LinuxHost/CDP.swift @@ -1,3 +1,4 @@ +import HeadlessProtocol import Foundation #if canImport(Darwin) import Darwin @@ -181,7 +182,7 @@ private final class RawDevToolsPipe: CDPTransport, @unchecked Sendable { private let inputDescriptor: Int32 private let outputDescriptor: Int32 private let stateLock = NSLock() - private var pending: [UInt8] = [] + private var pending = NullTerminatedMessageBuffer() private var closed = false // Page.captureScreenshot returns base64 in a CDP response. This is an // internal browser pipe (not the 1 MiB agent socket), so it needs room for @@ -228,15 +229,13 @@ private final class RawDevToolsPipe: CDPTransport, @unchecked Sendable { func receiveText(timeoutMilliseconds: Int32) throws -> String { while true { - if let terminator = pending.firstIndex(of: 0) { - let message = Array(pending[.. 0 else { throw CDPError.invalidResponse("DevTools pipe closed") } + guard pending.bufferedByteCount <= maximumMessageBytes - count else { + throw CDPError.invalidResponse("DevTools pipe message too large") + } pending.append(contentsOf: buffer[..) { + storage.append(contentsOf: bytes) + } + + package mutating func popFirst() -> [UInt8]? { + guard let terminator = storage[scanOffset...].firstIndex(of: 0) else { + scanOffset = storage.endIndex + return nil + } + let message = Array(storage[messageStart..= 65_536, messageStart >= storage.count / 2 { + let removed = messageStart + storage.removeFirst(removed) + messageStart = 0 + scanOffset -= removed + } + } +} diff --git a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift index a89437e..3a660fd 100644 --- a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift +++ b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift @@ -1511,6 +1511,34 @@ struct ProtocolTests { _ = requestFinished.wait(timeout: .now() + 2) } + static func nullTerminatedBufferScansIncrementally() throws { + var buffer = NullTerminatedMessageBuffer() + let chunk = [UInt8](repeating: 0x61, count: 8_192) + let chunkSlice = chunk[...] + let chunkCount = 30 * 1_024 * 1_024 / chunk.count + + for _ in 0..