Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
33443b1
cmux ssh: test that replay and reconnect filters drop OSC 52 clipboar…
austinywang Sep 30, 2026
6784022
cmux ssh: drop replayed OSC 52 clipboard reads and their reconnect re…
austinywang Sep 30, 2026
ae41937
cmux ssh: test that a read gap inside an OSC 52 reply keeps discarding
austinywang Sep 30, 2026
98f1449
cmux ssh: keep discarding an OSC 52 reply across a read gap
austinywang Sep 30, 2026
0d7f4e5
cmux ssh: test that the live release manifest cannot override the emb…
austinywang Sep 30, 2026
6f5ad60
cmux ssh: never let the live release manifest override the embedded d…
austinywang Sep 30, 2026
30c7650
cmux vm ssh: test that an unlaunched SSH startup script is removed
austinywang Sep 30, 2026
c256079
cmux vm ssh: remove SSH startup scripts no terminal will run
austinywang Sep 30, 2026
6d26925
cmux ssh relay: test that the remote CLI requires the relay to prove …
austinywang Sep 30, 2026
f4837ed
cmux ssh relay: make the relay prove it holds the token
austinywang Sep 30, 2026
4c6a002
cmux ssh relay: test that idle pre-auth connections cannot block the …
austinywang Sep 30, 2026
15a0916
cmux ssh relay: give unauthenticated connections their own budget
austinywang Sep 30, 2026
0f5dfcf
cmux ssh: test that differently routed connections do not share a master
austinywang Sep 30, 2026
887f0ae
cmux ssh: give each security-relevant route its own control socket
austinywang Sep 30, 2026
622c9b2
cmux ssh relay: share the relay handshake between the app and the mac…
austinywang Sep 30, 2026
b8b1e73
cmux ssh relay: test that the macOS CLI requires the relay to prove t…
austinywang Sep 30, 2026
6a925f0
cmux ssh relay: make the macOS CLI require the relay to prove the token
austinywang Sep 30, 2026
632e8fc
cmux ssh: compare the PTY bridge token in constant time
austinywang Sep 30, 2026
c7801d3
cmux ssh relay: make relay authentication an instantiated value
austinywang Sep 30, 2026
ba50fb1
cmux ssh: test that a stalled or oversized replay cannot swallow inpu…
austinywang Sep 30, 2026
0acc306
cmux ssh: bound the attach replay phase and never drop buffered replay
austinywang Sep 30, 2026
06b1909
cmux ssh: test that relayed remote status withholds the local window
austinywang Sep 30, 2026
48e1128
cmux ssh: withhold the local window from relayed remote status
austinywang Sep 30, 2026
bc93864
cmux ssh: test that npm bootstrap verifies the pinned binary digest
austinywang Sep 30, 2026
e7ebb27
cmux ssh: verify npm-installed remote binaries against pinned digests
austinywang Sep 30, 2026
a399afc
cmux ssh: test that squatted /tmp socket directories do not block sta…
austinywang Sep 30, 2026
8f57994
cmux ssh: fall back to a private socket directory when /tmp is squatted
austinywang Sep 30, 2026
ecac151
test(hermes): prime fixture executables before the timed wrapper launch
austinywang Sep 30, 2026
191b525
cmux ssh: count the auth cleanup fork budget by real uid
austinywang Sep 30, 2026
b88013b
cmux ssh: test that a stalled-replay deadline keeps the replay query …
austinywang Sep 30, 2026
ce0874c
cmux ssh: keep stripping replay queries after the replay deadline
austinywang Sep 30, 2026
29aa234
cmux ssh: test that a stop request mid clipboard reply drops the rest
austinywang Sep 30, 2026
1ac877d
cmux ssh: keep discarding a clipboard reply after a reconnect stop
austinywang Sep 30, 2026
5fbb7ed
cmux ssh: test that bootstrap works when the remote login shell is no…
austinywang Sep 30, 2026
48a0326
cmux ssh: run bootstrap shell scripts under sh -c on any login shell
austinywang Sep 30, 2026
ec14967
cmux ssh: test that aliases with a %n proxy do not share a master
austinywang Sep 30, 2026
e45aec6
cmux ssh: key route-specific masters by the destination alias
austinywang Sep 30, 2026
4e15f1e
cmux ssh: test that a stalled reconnect replay can drop its partial p…
austinywang Sep 30, 2026
efc5c9c
cmux ssh: drop a partial reconnect prefix when the replay deadline fires
austinywang Sep 30, 2026
f5ba754
cmux ssh: test that SSH launchers are created owner-only without foll…
austinywang Sep 30, 2026
3f49e5e
cmux ssh: create SSH launchers owner-only with O_EXCL and O_NOFOLLOW
austinywang Sep 30, 2026
cc55b32
test(hermes): prime the fixtures the test wrote instead of scanning tmp
austinywang Sep 30, 2026
e11dc98
cmux ssh: test that a stdout notice before the npm digest is not tamp…
austinywang Sep 30, 2026
bd6c812
cmux ssh: read the pinned npm digest from a marker line
austinywang Sep 30, 2026
ef56c4f
cmux ssh: test that a delayed publisher keeps the recorded socket dir…
austinywang Sep 30, 2026
7962f5d
cmux ssh: replace a stale socket-dir record only under its lock
austinywang Sep 30, 2026
4214178
cmux ssh: test that suppressed replay bytes move the query filter bou…
austinywang Sep 30, 2026
ffcf732
cmux ssh: move the replay query boundary past suppressed replay bytes
austinywang Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/cmux-tui-build-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -611,9 +611,13 @@ jobs:
- name: Build npm package directories
if: inputs.package_npm
run: |
# The build jobs stamp this same checkout's HEAD as
# CMUX_TUI_BUILD_COMMIT; the SSH bootstrap accepts the pinned
# remote-binary digests only for that build identity.
python3 cmux-tui/dist/scripts/package_npm.py \
--binaries-dir dist/binaries \
--version "$NPM_VERSION" \
--build-commit "$(git rev-parse HEAD)" \
${{ inputs.include_windows && '--include-windows' || '' }} \
--out dist/npm-packages

Expand Down
18 changes: 9 additions & 9 deletions CLI/CMUXCLI+SSHStartupScripts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ extension CMUXCLI {
isShellSnippet: Bool = false,
passwordCredential: String? = nil,
controlPathPreflightShellFunction: String? = nil,
reconnectLimitDefault: Int = 20
reconnectLimitDefault: Int = 20,
launchScripts: SSHStartupLaunchScripts
) throws -> String {
let script = buildSSHStartupScriptBody(
sshCommand: sshCommand,
Expand All @@ -21,7 +22,7 @@ extension CMUXCLI {
oneTimeCommand: nil,
reconnectLimitDefault: reconnectLimitDefault
)
return try writeSSHStartupScript(script, remoteRelayPort: remoteRelayPort)
return try writeSSHStartupScript(script, remoteRelayPort: remoteRelayPort, launchScripts: launchScripts)
}

func buildReusableSSHStartupCommand(
Expand Down Expand Up @@ -423,13 +424,12 @@ extension CMUXCLI {
]
return scriptLines.joined(separator: "\n")
}
private func writeSSHStartupScript(_ scriptBody: String, remoteRelayPort: Int) throws -> String {
let scriptURL = FileManager.default.temporaryDirectory.appendingPathComponent(
"cmux-ssh-startup-\(remoteRelayPort)-\(UUID().uuidString.lowercased()).sh"
)
let script = "#!/bin/sh\n\(scriptBody)\n"
try script.write(to: scriptURL, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: scriptURL.path)
private func writeSSHStartupScript(
_ scriptBody: String,
remoteRelayPort: Int,
launchScripts: SSHStartupLaunchScripts
) throws -> String {
let scriptURL = try launchScripts.write(scriptBody: scriptBody, remoteRelayPort: remoteRelayPort)
return shellQuote(scriptURL.path)
}
private func reusableShellStartupCommand(
Expand Down
21 changes: 18 additions & 3 deletions CLI/SSHPTYAttachReconnectInputFilter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ final class SSHPTYAttachReconnectInputFilter {
var reconnectInputFilter = reconnectInputFilterState.map(SSHPTYAttachReconnectInputFilter.init(state:))
var stopSignalFD = initialStopSignalFD
var stopAcknowledgementFD = initialStopAcknowledgementFD
var reconnectStopAcknowledged = false
var buffer = [UInt8](repeating: 0, count: 8192)
defer {
if let stopSignalFD {
Expand Down Expand Up @@ -164,6 +165,15 @@ final class SSHPTYAttachReconnectInputFilter {
defer {
acknowledgeStopFiltering()
closeStopSignal()
reconnectStopAcknowledged = true
}
// Callers flush pending probe bytes before stopping, so only a
// clipboard reply that is mid-discard can remain. Keep routing
// input through the filter until that reply's terminator (or the
// reconnect deadline) so its tail never reaches the remote PTY.
if let filter = reconnectInputFilter {
_ = filter.stopFiltering()
if filter.isFilteringActive { return true }
}
reconnectInputFilter = nil
return true
Expand All @@ -172,13 +182,14 @@ final class SSHPTYAttachReconnectInputFilter {
func finishStdin() {
// Reconnect input can disappear with the old bridge during wake.
// It is not an intentional EOF for the newly attached remote PTY.
guard reconnectInputFilter == nil else { return }
guard reconnectInputFilter == nil || reconnectStopAcknowledged else { return }
_ = shutdown(fd, SHUT_WR)
}

func stopReconnectFilteringAtDeadline() async -> Bool {
guard let filter = reconnectInputFilter else { return true }
guard await writeOrShutdown(filter.stopFiltering()) else { return false }
guard await writeOrShutdown(filter.stopFilteringAtDeadline()) else { return false }
reconnectInputFilter = nil
return stopReconnectFiltering()
}

Expand Down Expand Up @@ -273,7 +284,7 @@ final class SSHPTYAttachReconnectInputFilter {

func filter(_ data: Data) -> Data {
if isDeadlineReached {
var output = stopFiltering()
var output = stopFilteringAtDeadline()
output.append(data)
return output
}
Expand All @@ -288,6 +299,10 @@ final class SSHPTYAttachReconnectInputFilter {
byteFilter.stopFiltering()
}

func stopFilteringAtDeadline() -> Data {
byteFilter.stopFilteringAtDeadline()
}

var hasPendingInput: Bool {
byteFilter.hasPendingInput
}
Expand Down
Loading
Loading