From 2df6ed94af278ceb72379971de82e37e94c768b7 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Fri, 11 Sep 2026 11:37:30 +0530 Subject: [PATCH 1/2] 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. --- src/cm/lsp/serverLauncher.ts | 9 +++++- src/plugins/terminal/scripts/init-sandbox.sh | 32 +++++++++++++++++--- src/settings/lspServerDetail.js | 15 ++++----- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/cm/lsp/serverLauncher.ts b/src/cm/lsp/serverLauncher.ts index b6c3058111..bc83251a63 100644 --- a/src/cm/lsp/serverLauncher.ts +++ b/src/cm/lsp/serverLauncher.ts @@ -40,6 +40,13 @@ const STATUS_FAILED: InstallStatus = "failed"; const DONT_ASK_TERMINAL_REQUIRED_FOR_LSP = "dontAskTerminalRequiredForLsp"; +// PRoot prints `can't sanitize binding "/proc/self/fd/N"` when one of the +// session's stdio descriptors is a pipe/socket and therefore cannot be +// canonicalized (see init-sandbox.sh). Drop only that known, harmless message +// so genuine proot warnings are still surfaced in the LSP log. +const PROOT_FD_BINDING_WARNING = + /can'?t sanitize binding "\/proc\/self\/fd(?:\/[012])?"/i; + let alreadyInformed = false; function getTerminalRequiredMessage(): string { @@ -911,7 +918,7 @@ async function startInteractiveServer( ): Promise { const executor = getExecutor(); const callback: ExecutorCallback = (type, data) => { - if (type === "stderr" && /proot warning/i.test(data)) return; + if (type === "stderr" && PROOT_FD_BINDING_WARNING.test(data)) return; if (type === "stdout" && /listening on/i.test(data)) { signalServerReady(serverId); } diff --git a/src/plugins/terminal/scripts/init-sandbox.sh b/src/plugins/terminal/scripts/init-sandbox.sh index 5e100ef9e4..509aa2d561 100644 --- a/src/plugins/terminal/scripts/init-sandbox.sh +++ b/src/plugins/terminal/scripts/init-sandbox.sh @@ -69,19 +69,43 @@ ARGS="$ARGS -b $PREFIX/public:/root" ARGS="$ARGS -b $PREFIX/alpine/tmp:/dev/shm" -if [ -e "/proc/self/fd" ]; then +# PRoot canonicalizes every -b host path with realpath(3). The magic links +# under /proc/self/fd only resolve when the descriptor points at a real file: +# for pipes, sockets, anon inodes or memfds the link target is not a path +# ("pipe:[123]"), so realpath(3) fails with ENOENT, proot drops the binding and +# prints `can't sanitize binding "/proc/self/fd/N"`. `[ -e ]` follows the magic +# link to the underlying inode and therefore returns true for those descriptors, +# which is why it does not filter them out. Test the link target the same way +# realpath(3) does instead. +can_bind() { + # Directories (e.g. /proc/self/fd) are canonicalizable as-is. + if [ -d "$1" ]; then + return 0 + fi + + # A /proc/self/fd/N magic link is only canonicalizable when it resolves to + # an existing absolute path; "pipe:[N]", "socket:[N]" and friends are not. + target=$(readlink "$1" 2>/dev/null) || return 1 + + case "$target" in + /*) [ -e "$target" ] ;; + *) return 1 ;; + esac +} + +if can_bind /proc/self/fd; then ARGS="$ARGS -b /proc/self/fd:/dev/fd" fi -if [ -e "/proc/self/fd/0" ]; then +if can_bind /proc/self/fd/0; then ARGS="$ARGS -b /proc/self/fd/0:/dev/stdin" fi -if [ -e "/proc/self/fd/1" ]; then +if can_bind /proc/self/fd/1; then ARGS="$ARGS -b /proc/self/fd/1:/dev/stdout" fi -if [ -e "/proc/self/fd/2" ]; then +if can_bind /proc/self/fd/2; then ARGS="$ARGS -b /proc/self/fd/2:/dev/stderr" fi diff --git a/src/settings/lspServerDetail.js b/src/settings/lspServerDetail.js index b4f431a851..c84a2a2c7b 100644 --- a/src/settings/lspServerDetail.js +++ b/src/settings/lspServerDetail.js @@ -154,18 +154,19 @@ function formatStartupTimeoutValue(timeout) { : strings["lsp-default"]; } +// PRoot prints `can't sanitize binding "/proc/self/fd/N"` when one of the +// session's stdio descriptors is a pipe/socket and therefore cannot be +// canonicalized (see init-sandbox.sh). Drop only that known, harmless message +// so genuine proot warnings still reach the UI. +const PROOT_FD_BINDING_WARNING = + /can'?t sanitize binding "\/proc\/self\/fd(?:\/[012])?"/i; + function sanitizeInstallMessage(message) { const lines = String(message || "") .split("\n") .map((line) => line.trim()) .filter(Boolean) - .filter( - (line) => - !/^proot warning:/i.test(line) && - !line.includes(`"/proc/self/fd/0"`) && - !line.includes(`"/proc/self/fd/1"`) && - !line.includes(`"/proc/self/fd/2"`), - ); + .filter((line) => !PROOT_FD_BINDING_WARNING.test(line)); return lines.join(" "); } From 410466abc49027f8e54368710f4d35fe1e87aeaa Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Fri, 11 Sep 2026 11:51:08 +0530 Subject: [PATCH 2/2] 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. --- src/plugins/terminal/scripts/init-sandbox.sh | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/plugins/terminal/scripts/init-sandbox.sh b/src/plugins/terminal/scripts/init-sandbox.sh index 509aa2d561..1da53a95b4 100644 --- a/src/plugins/terminal/scripts/init-sandbox.sh +++ b/src/plugins/terminal/scripts/init-sandbox.sh @@ -77,6 +77,14 @@ ARGS="$ARGS -b $PREFIX/alpine/tmp:/dev/shm" # link to the underlying inode and therefore returns true for those descriptors, # which is why it does not filter them out. Test the link target the same way # realpath(3) does instead. +# +# Probe through this shell's own pid ($$), not /proc/self: 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 below rather than the stderr proot inherits. $$ is +# unchanged by the exec at the end of this script, so it always names the +# process proot will run as. +SELF_PID=$$ + can_bind() { # Directories (e.g. /proc/self/fd) are canonicalizable as-is. if [ -d "$1" ]; then @@ -93,19 +101,19 @@ can_bind() { esac } -if can_bind /proc/self/fd; then +if can_bind "/proc/$SELF_PID/fd"; then ARGS="$ARGS -b /proc/self/fd:/dev/fd" fi -if can_bind /proc/self/fd/0; then +if can_bind "/proc/$SELF_PID/fd/0"; then ARGS="$ARGS -b /proc/self/fd/0:/dev/stdin" fi -if can_bind /proc/self/fd/1; then +if can_bind "/proc/$SELF_PID/fd/1"; then ARGS="$ARGS -b /proc/self/fd/1:/dev/stdout" fi -if can_bind /proc/self/fd/2; then +if can_bind "/proc/$SELF_PID/fd/2"; then ARGS="$ARGS -b /proc/self/fd/2:/dev/stderr" fi