Skip to content

Commit 2e154b3

Browse files
fix(terminal): stop proot "can't sanitize binding" fd warnings (#2878)
* fix(terminal): stop proot "can't sanitize binding" fd warnings proot canonicalizes every -b host path with realpath(3). The /proc/self/fd/N magic links only resolve when the descriptor points to a real file, so piped stdio (what Acode's ProcessBuilder uses) made realpath(3) fail and proot drop the binding with `can't sanitize binding "/proc/self/fd/N"`. The [ -e ] guard passed those links because stat(2) follows the magic link to the underlying pipe/socket inode, so it never filtered them out. Replace it with a can_bind helper that resolves the link target the way realpath(3) does, and narrow the LSP-side proot-warning filters to only this benign message so real proot warnings are no longer swallowed. * fix(terminal): probe /proc/self/fd via shell pid in can_bind readlink(1) runs as a child process, so for fd 2 its /proc/self/fd/2 is the /dev/null of the `2>/dev/null` redirect rather than the stderr proot inherits. That made can_bind treat a piped stderr as bindable, leaving one `can't sanitize binding "/proc/self/fd/2"` warning. Probe /proc/$$/fd/N instead; $$ survives the exec and names the process proot runs as.
1 parent efae4a9 commit 2e154b3

3 files changed

Lines changed: 52 additions & 12 deletions

File tree

src/cm/lsp/serverLauncher.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ const STATUS_FAILED: InstallStatus = "failed";
4040

4141
const DONT_ASK_TERMINAL_REQUIRED_FOR_LSP = "dontAskTerminalRequiredForLsp";
4242

43+
// PRoot prints `can't sanitize binding "/proc/self/fd/N"` when one of the
44+
// session's stdio descriptors is a pipe/socket and therefore cannot be
45+
// canonicalized (see init-sandbox.sh). Drop only that known, harmless message
46+
// so genuine proot warnings are still surfaced in the LSP log.
47+
const PROOT_FD_BINDING_WARNING =
48+
/can'?t sanitize binding "\/proc\/self\/fd(?:\/[012])?"/i;
49+
4350
let alreadyInformed = false;
4451

4552
function getTerminalRequiredMessage(): string {
@@ -911,7 +918,7 @@ async function startInteractiveServer(
911918
): Promise<string> {
912919
const executor = getExecutor();
913920
const callback: ExecutorCallback = (type, data) => {
914-
if (type === "stderr" && /proot warning/i.test(data)) return;
921+
if (type === "stderr" && PROOT_FD_BINDING_WARNING.test(data)) return;
915922
if (type === "stdout" && /listening on/i.test(data)) {
916923
signalServerReady(serverId);
917924
}

src/plugins/terminal/scripts/init-sandbox.sh

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,51 @@ ARGS="$ARGS -b $PREFIX/public:/root"
6969
ARGS="$ARGS -b $PREFIX/alpine/tmp:/dev/shm"
7070

7171

72-
if [ -e "/proc/self/fd" ]; then
72+
# PRoot canonicalizes every -b host path with realpath(3). The magic links
73+
# under /proc/self/fd only resolve when the descriptor points at a real file:
74+
# for pipes, sockets, anon inodes or memfds the link target is not a path
75+
# ("pipe:[123]"), so realpath(3) fails with ENOENT, proot drops the binding and
76+
# prints `can't sanitize binding "/proc/self/fd/N"`. `[ -e ]` follows the magic
77+
# link to the underlying inode and therefore returns true for those descriptors,
78+
# which is why it does not filter them out. Test the link target the same way
79+
# realpath(3) does instead.
80+
#
81+
# Probe through this shell's own pid ($$), not /proc/self: readlink(1) runs as a
82+
# child process, so for fd 2 its /proc/self/fd/2 is the /dev/null of the
83+
# `2>/dev/null` redirect below rather than the stderr proot inherits. $$ is
84+
# unchanged by the exec at the end of this script, so it always names the
85+
# process proot will run as.
86+
SELF_PID=$$
87+
88+
can_bind() {
89+
# Directories (e.g. /proc/self/fd) are canonicalizable as-is.
90+
if [ -d "$1" ]; then
91+
return 0
92+
fi
93+
94+
# A /proc/self/fd/N magic link is only canonicalizable when it resolves to
95+
# an existing absolute path; "pipe:[N]", "socket:[N]" and friends are not.
96+
target=$(readlink "$1" 2>/dev/null) || return 1
97+
98+
case "$target" in
99+
/*) [ -e "$target" ] ;;
100+
*) return 1 ;;
101+
esac
102+
}
103+
104+
if can_bind "/proc/$SELF_PID/fd"; then
73105
ARGS="$ARGS -b /proc/self/fd:/dev/fd"
74106
fi
75107

76-
if [ -e "/proc/self/fd/0" ]; then
108+
if can_bind "/proc/$SELF_PID/fd/0"; then
77109
ARGS="$ARGS -b /proc/self/fd/0:/dev/stdin"
78110
fi
79111

80-
if [ -e "/proc/self/fd/1" ]; then
112+
if can_bind "/proc/$SELF_PID/fd/1"; then
81113
ARGS="$ARGS -b /proc/self/fd/1:/dev/stdout"
82114
fi
83115

84-
if [ -e "/proc/self/fd/2" ]; then
116+
if can_bind "/proc/$SELF_PID/fd/2"; then
85117
ARGS="$ARGS -b /proc/self/fd/2:/dev/stderr"
86118
fi
87119

src/settings/lspServerDetail.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,18 +154,19 @@ function formatStartupTimeoutValue(timeout) {
154154
: strings["lsp-default"];
155155
}
156156

157+
// PRoot prints `can't sanitize binding "/proc/self/fd/N"` when one of the
158+
// session's stdio descriptors is a pipe/socket and therefore cannot be
159+
// canonicalized (see init-sandbox.sh). Drop only that known, harmless message
160+
// so genuine proot warnings still reach the UI.
161+
const PROOT_FD_BINDING_WARNING =
162+
/can'?t sanitize binding "\/proc\/self\/fd(?:\/[012])?"/i;
163+
157164
function sanitizeInstallMessage(message) {
158165
const lines = String(message || "")
159166
.split("\n")
160167
.map((line) => line.trim())
161168
.filter(Boolean)
162-
.filter(
163-
(line) =>
164-
!/^proot warning:/i.test(line) &&
165-
!line.includes(`"/proc/self/fd/0"`) &&
166-
!line.includes(`"/proc/self/fd/1"`) &&
167-
!line.includes(`"/proc/self/fd/2"`),
168-
);
169+
.filter((line) => !PROOT_FD_BINDING_WARNING.test(line));
169170

170171
return lines.join(" ");
171172
}

0 commit comments

Comments
 (0)