diff --git a/tool/tsh/common/dup2_linux.go b/tool/tsh/common/dup2_linux.go
new file mode 100644
index 00000000000..52ca8b0585e
--- /dev/null
+++ b/tool/tsh/common/dup2_linux.go
@@ -0,0 +1,34 @@
+// Teleport
+// Copyright (C) 2025 Gravitational, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//go:build linux
+
+package common
+
+import "syscall"
+
+// dup2 implements syscall.Dup2(oldfd, newfd) in a way that works on all
+// current Linux platforms, and likely on any new platforms. New platforms
+// such as ARM64 do not implement syscall.Dup2() instead implementing
+// syscall.Dup3() which is largely a superset, with one special case.
+func dup2(oldfd, newfd int) error {
+ if oldfd == newfd {
+ // dup2 would do nothing in this case, but dup3 returns an error.
+ // Emulate dup2 behavior.
+ return nil
+ }
+ return syscall.Dup3(oldfd, newfd, 0)
+}
diff --git a/tool/tsh/common/dup2_unix.go b/tool/tsh/common/dup2_unix.go
new file mode 100644
index 00000000000..47aef968848
--- /dev/null
+++ b/tool/tsh/common/dup2_unix.go
@@ -0,0 +1,28 @@
+// Teleport
+// Copyright (C) 2025 Gravitational, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//go:build unix && !linux
+
+package common
+
+import "syscall"
+
+// dup2 wraps syscall.Dup2(oldfd, newfd) on non-linux unix platforms. The
+// linux implementation uses syscall.Dup3() as Dup2() is not available
+// on all linux platforms.
+func dup2(oldfd, newfd int) error {
+ return syscall.Dup2(oldfd, newfd)
+}
diff --git a/tool/tsh/common/reexec_unix.go b/tool/tsh/common/reexec_unix.go
index bbcf9a0d5a6..4a707ab8a62 100644
--- a/tool/tsh/common/reexec_unix.go
+++ b/tool/tsh/common/reexec_unix.go
@@ -51,16 +51,17 @@ func replaceStdin() (*os.File, error) {
}
var dupErr error
if ctrlErr := rc.Control(func(fd uintptr) {
- dupErr = syscall.Dup2(int(fd), syscall.Stdin)
- // stdin is not O_CLOEXEC after dup2 but thankfully the three stdio
- // file descriptors must be not O_CLOEXEC anyway, so we can avoid
- // a linux-specific implementation or syscall.ForkLock shenanigans
+ dupErr = dup2(int(fd), syscall.Stdin)
+ // dup2() is sufficient here as the three stdio file
+ // descriptors must not be O_CLOEXEC. Darwin does not have
+ // dup3(), so would need to resort to syscall.ForkLock
+ // shenanigans if we did need to set O_CLOEXEC.
}); ctrlErr != nil {
_ = devNull.Close()
return nil, trace.Wrap(ctrlErr)
}
if dupErr != nil {
- // this is the error from Dup2
+ // this is the error from dup2
_ = devNull.Close()
return nil, trace.Wrap(err)
}