为什么 `sudo bash <(curl ...)` 会失败?谈谈 Bash 进程替换与 sudo 的边界问题
为什么 sudo bash <(curl ...) 会失败?谈谈 Bash 进程替换与 sudo 的边界问题
起因
最近在 Ubuntu 24.04 上安装 Docker,按照 linuxmirrors.cn 给出的官方一键脚本执行:
sudo bash <(curl -sSL https://linuxmirrors.cn/docker.sh)
结果遇到了一个看起来很奇怪的报错:
bash: /dev/fd/63: No such file or directory
curl: (23) Failure writing output to destination
一开始我把怀疑方向放在了 Snap 版 curl、AppArmor、Ubuntu 24.04 的 user namespace 限制,甚至 Miniconda 的 shell 初始化上。但最后发现,问题的本质和这些都没关系——它是 Bash 进程替换 (<(...)) 与 sudo 的机制冲突。
这篇博客把过程和结论整理出来,希望能帮到遇到同样问题的人。
现象复现
失败的命令(官网推荐写法):
sudo bash <(curl -sSL https://linuxmirrors.cn/docker.sh)
报错:
bash: /dev/fd/63: No such file or directory
curl: (23) Failure writing output to destination
而下面这个命令是正常的:
curl -sSL https://linuxmirrors.cn/docker.sh -o docker.sh
sudo bash docker.sh
同样是把脚本下载下来交给 root 的 bash 执行,为什么前者就不行?
<(...) 到底做了什么?
<(command) 是 Bash 的 进程替换 (process substitution)。它的工作机制大致是:
- 当前 shell(也就是你这个普通用户的 bash)fork 出一个子进程去执行
command。 - Bash 把这个子进程的 stdout 连接到一个匿名管道(或在某些系统上是 FIFO)。
- Bash 把这个管道暴露成一个伪文件路径,比如
/dev/fd/63。 - 这个路径被替换到原命令行中。
所以你以为执行的是:
sudo bash <(curl ...)
实际 Bash 展开后是:
sudo bash /dev/fd/63
而 /dev/fd/63 是当前用户 shell 创建的文件描述符,并不是磁盘上的真实文件。
为什么 sudo 之后就读不到了?
关键就在这里:
/dev/fd/63是 你当前 shell 进程 的第 63 号 FD。- 通过
sudo启动的bash是一个新的、运行在 root 上下文 下的进程。 sudo出于安全考虑,会清理执行环境:关闭多余的文件描述符、重置环境变量、切换 UID。
于是 root 的 bash 拿着路径 /dev/fd/63 去打开时,那个 FD 在它自己的进程里根本不存在,自然就:
bash: /dev/fd/63: No such file or directory
而 curl 那边还在往管道写数据,发现读端已经断了,于是报告:
curl: (23) Failure writing output to destination
一句话总结:
<(...)是 当前 shell 的进程内对象,而sudo切换了进程上下文,新进程看不到它。
为什么 curl -o file && sudo bash file 就没问题?
因为这种写法下,脚本被真实地写到了磁盘上。sudo bash docker.sh 打开的是一个 基于路径的真实文件,跟 FD 继承、进程上下文都没关系,root 当然能正常读到。
正确的写法
避开这个坑,有两种推荐写法:
方式一:管道
curl -sSL https://linuxmirrors.cn/docker.sh | sudo bash
curl 的输出通过管道直接喂给 sudo bash 的 stdin,sudo 不会去关闭 stdin,bash 从 stdin 读脚本执行,完全没有 FD 继承的问题。
方式二:命令替换
sudo bash -c "$(curl -sSL https://linuxmirrors.cn/docker.sh)"
$(...) 是 命令替换,它会先把 curl 的输出展开成一段字符串,再作为参数传给 sudo bash -c。整个脚本作为字符串传过去,不依赖任何 FD。
方式三(手动挡):先下载再执行
curl -sSL https://linuxmirrors.cn/docker.sh -o docker.sh
sudo bash docker.sh
一些可以自己验证的诊断命令
如果你想直观看到这个机制,可以试试:
# 看进程替换生成的伪路径
echo <(echo test)
# 输出类似:/dev/fd/63
# 当前 shell 自己的 FD
ls -l /proc/$$/fd
# sudo 之后的 shell 看到的 FD(已经是另一个进程了)
sudo ls -l /proc/self/fd

你会清楚地看到:当前 shell 的 FD 表和 sudo 启动的子进程的 FD 表是两套,不会跨进程继承用户态的进程替换 FD。
总结
这个问题不是 Ubuntu 24.04 的 bug,也不是 curl 的 bug,更不是 AppArmor 或 Conda 干扰的。它是 Bash 进程替换的语义和 sudo 的安全模型 在交叉点上的天然边界:
- 进程替换的
/dev/fd/N只在创建它的进程里有效。 - sudo 跨越了进程边界,并刻意收紧了环境。
所以官网给出的 sudo bash <(curl ...) 这种写法,在某些发行版/某些 sudo 配置下能跑(取决于 sudo 是否保留了 FD、/dev/fd/ 实现是否走 /proc/self/fd 的符号链接等细节),但它本质上是不可靠的。
推荐改用:
curl -sSL https://linuxmirrors.cn/docker.sh | sudo bash
(看上去没有那么优雅但足够可靠)
写在最后
排障的时候很容易陷入 “症状归因偏差”——看到 Ubuntu 24.04 的新版本、看到 Snap 的 curl、看到 conda 环境,就下意识把问题往这些方向乱想。但真正高效的思路是先问一句:
“这个路径
/dev/fd/63到底是谁创建的?sudo 之后它还存在吗?”
把"是谁、在哪个进程上下文、什么生命周期"问清楚
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)