From 7b3031e8cd432f5f58228fbbd537d5f18cd4ba18 Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Thu, 20 Aug 2026 00:56:37 +0530 Subject: [PATCH 1/3] Set the interactive pty slave to raw mode The pty slave allocated for interactive containers was left in canonical mode, whose line discipline buffers output and causes large writes to be truncated at 1024 bytes (reported in apple/container#1148). A process writing more than 1024 bytes to stdout and exiting loses the buffered tail because the canonical-mode tty holds it waiting for a line terminator. Configure the slave with the existing Terminal.setraw() recipe (cfmakeraw plus OPOST preserved) before dup3'ing it onto stdio, so output passes through without truncation while newline-to-CRLF translation is kept. Add a regression test asserting the termios flags: a fresh pty slave is canonical, and setraw() clears ICANON while preserving OPOST. --- .../TerminalRawModeTests.swift | 69 +++++++++++++++++++ vminitd/Sources/vmexec/Console.swift | 7 ++ 2 files changed, 76 insertions(+) create mode 100644 Tests/ContainerizationOSTests/TerminalRawModeTests.swift diff --git a/Tests/ContainerizationOSTests/TerminalRawModeTests.swift b/Tests/ContainerizationOSTests/TerminalRawModeTests.swift new file mode 100644 index 000000000..7e6e10773 --- /dev/null +++ b/Tests/ContainerizationOSTests/TerminalRawModeTests.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation +import Testing + +@testable import ContainerizationOS + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#endif + +@Suite("Terminal raw mode tests") +struct TerminalRawModeTests { + private func termiosAttributes(of descriptor: Int32) throws -> termios { + var attrs = termios() + try #require(tcgetattr(descriptor, &attrs) == 0, "tcgetattr failed, errno: \(errno)") + return attrs + } + + private func has(_ flag: tcflag_t, in attributes: termios) -> Bool { + attributes.c_lflag & flag != 0 + } + + @Test("pty slave is in canonical mode before raw-mode setup") + func ptySlaveIsCanonicalBeforeSetup() throws { + let (parent, child) = try Terminal.create(initialSize: Terminal.Size(width: 120, height: 40)) + defer { + try? parent.close() + try? child.close() + } + + // A freshly allocated pty slave is canonical by default (ICANON set). + let attrs = try termiosAttributes(of: child.handle.fileDescriptor) + #expect(has(tcflag_t(ICANON), in: attrs)) + } + + @Test("setraw clears ICANON and preserves OPOST") + func setrawClearsCanonicalAndPreservesOutputPostProcessing() throws { + let (parent, child) = try Terminal.create(initialSize: Terminal.Size(width: 120, height: 40)) + defer { + try? parent.close() + try? child.close() + } + + try child.setraw() + + let attrs = try termiosAttributes(of: child.handle.fileDescriptor) + #expect(!has(tcflag_t(ICANON), in: attrs), "setraw must clear canonical mode") + #expect(attrs.c_oflag & tcflag_t(OPOST) != 0, "setraw must preserve OPOST for CRLF output translation") + } +} diff --git a/vminitd/Sources/vmexec/Console.swift b/vminitd/Sources/vmexec/Console.swift index b868049f5..95541af46 100644 --- a/vminitd/Sources/vmexec/Console.swift +++ b/vminitd/Sources/vmexec/Console.swift @@ -14,6 +14,7 @@ // limitations under the License. //===----------------------------------------------------------------------===// +import ContainerizationOS import FoundationEssentials import LCShim @@ -55,6 +56,12 @@ class Console { } defer { _ = _close(slaveFD) } + // Put the pty slave into raw mode so the tty line discipline does not + // buffer or truncate large output writes (the container's stdout goes + // through this slave). `Terminal.setraw()` applies cfmakeraw and keeps + // OPOST, so newline translation to CRLF on output is preserved. + try Terminal(descriptor: slaveFD, setInitState: false).setraw() + for fd: Int32 in 0...2 { guard dup3(slaveFD, fd, 0) != -1 else { throw App.Errno(stage: "dup3") From a5be6bc3a30afbea444a2bd5b7f7fee41c555a1c Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Thu, 20 Aug 2026 01:01:05 +0530 Subject: [PATCH 2/3] Consolidate termios flag assertions in pty raw-mode test Unify the ICANON and OPOST assertions through one flag-membership helper operating on a tcflag_t field, instead of re-inlining the same bit-mask idiom with different shapes. --- .../ContainerizationOSTests/TerminalRawModeTests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/ContainerizationOSTests/TerminalRawModeTests.swift b/Tests/ContainerizationOSTests/TerminalRawModeTests.swift index 7e6e10773..f7b576908 100644 --- a/Tests/ContainerizationOSTests/TerminalRawModeTests.swift +++ b/Tests/ContainerizationOSTests/TerminalRawModeTests.swift @@ -35,8 +35,8 @@ struct TerminalRawModeTests { return attrs } - private func has(_ flag: tcflag_t, in attributes: termios) -> Bool { - attributes.c_lflag & flag != 0 + private func has(_ flag: tcflag_t, in flags: tcflag_t) -> Bool { + flags & flag != 0 } @Test("pty slave is in canonical mode before raw-mode setup") @@ -49,7 +49,7 @@ struct TerminalRawModeTests { // A freshly allocated pty slave is canonical by default (ICANON set). let attrs = try termiosAttributes(of: child.handle.fileDescriptor) - #expect(has(tcflag_t(ICANON), in: attrs)) + #expect(has(tcflag_t(ICANON), in: attrs.c_lflag)) } @Test("setraw clears ICANON and preserves OPOST") @@ -63,7 +63,7 @@ struct TerminalRawModeTests { try child.setraw() let attrs = try termiosAttributes(of: child.handle.fileDescriptor) - #expect(!has(tcflag_t(ICANON), in: attrs), "setraw must clear canonical mode") - #expect(attrs.c_oflag & tcflag_t(OPOST) != 0, "setraw must preserve OPOST for CRLF output translation") + #expect(!has(tcflag_t(ICANON), in: attrs.c_lflag), "setraw must clear canonical mode") + #expect(has(tcflag_t(OPOST), in: attrs.c_oflag), "setraw must preserve OPOST for CRLF output translation") } } From 9ed4b36020b8d17acc3994a50c7be72a43e963a1 Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Thu, 20 Aug 2026 01:19:58 +0530 Subject: [PATCH 3/3] Assert ONLCR preservation in pty raw-mode test cfmakeraw clears OPOST but leaves ONLCR set; re-adding OPOST in Terminal.setraw() restores newline-to-CRLF output translation. Lock both flags so the raw-mode recipe's output behavior is pinned by the test. --- Tests/ContainerizationOSTests/TerminalRawModeTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/ContainerizationOSTests/TerminalRawModeTests.swift b/Tests/ContainerizationOSTests/TerminalRawModeTests.swift index f7b576908..7ad1f5ed3 100644 --- a/Tests/ContainerizationOSTests/TerminalRawModeTests.swift +++ b/Tests/ContainerizationOSTests/TerminalRawModeTests.swift @@ -64,6 +64,7 @@ struct TerminalRawModeTests { let attrs = try termiosAttributes(of: child.handle.fileDescriptor) #expect(!has(tcflag_t(ICANON), in: attrs.c_lflag), "setraw must clear canonical mode") - #expect(has(tcflag_t(OPOST), in: attrs.c_oflag), "setraw must preserve OPOST for CRLF output translation") + #expect(has(tcflag_t(OPOST), in: attrs.c_oflag), "setraw must preserve OPOST for output post-processing") + #expect(has(tcflag_t(ONLCR), in: attrs.c_oflag), "setraw must preserve ONLCR for CRLF output translation") } }