让 Linux 临时文件避开竞争和自动清理陷阱

临时文件的难点不只是“用完后删除”。同一台机器上的程序可能争用文件名,长时间运行的任务可能撞上自动清理,内存文件系统也可能耗尽空间。systemd 的《Using /tmp/ and /var/tmp/ Safely》把这些问题放在一起:先选对存放位置,再用安全的创建方式,最后明确谁负责资源的整个生命周期。

本文根据 systemd 官方原文翻译整理。原页未标个人作者,归属 systemd 项目;源文标注 LGPL-2.1-or-later。本文保留这一声明,注明中文翻译及版本校订。本文仅做静态核对,没有更改服务、文件系统或清理规则。

临时资源选择图:单个文件优先匿名文件和持有描述符,多文件目录使用随机私有目录并持有 flock,服务额外使用 PrivateTmp 隔离,退出后交由关闭描述符和清理器收尾
临时资源的创建、存活和回收路径。未完纪根据本文自绘的技术示意图,并非系统截图。

先分清目录承诺了什么

/tmp/ 与 /var/tmp/ 都是 Linux 提供的公共可写临时目录。常见系统把前者放在 tmpfs 上,数据占用内存或交换空间,并在重启时清空;后者通常位于持久文件系统上。因而,原文建议只把体积小且有明确大小上限的文件放进 /tmp/,其他临时数据考虑 /var/tmp/;需要跨重启保留的数据不能依赖 /tmp/。

“通常”不是保证。实际挂载方式、发行版规则和管理员配置才决定行为。/var/tmp/ 也不是长期存档目录,它仍可能接受定期清理。如果应用收到可信环境中的 TMPDIR,应优先使用该路径,不要硬编码这两个目录;涉及提权或不可信调用者时,还应由程序自己的安全策略判断是否接受环境变量。目录层级的具体用途可查 file-hierarchy(7)。

公共名称空间里,固定名字就是竞争入口

假设程序准备创建 /tmp/foo。另一名本地用户或另一个实例可以提前占用这个名字,令程序失败;更危险的实现还可能接受对方预先放好的文件或符号链接。先检查不存在、随后再创建,仍然留下检查与使用之间的竞争窗口。可预测文件名至少会造成拒绝服务风险,错误的打开方式还可能把读写导向不可信对象。

应把随机选择名字和安全创建交给合适的 API:POSIX 的 mkstemp()、mkdtemp(),glibc 的 mkostemp(),以及标准 C 的 tmpfile()。Linux 还可以使用带 O_TMPFILE 的 open(),或者 memfd_create()。最后一种直接创建由内存/交换空间支持的匿名文件,不经过 /tmp/ 或 /var/tmp/ 的公共路径。

原文提到一种例外:在启动早期由 tmpfiles.d 预建专用子目录,再在里面使用固定名字,X11 的 /tmp/.X11-unix/ 属于类似做法。不过如果软件包能够在运行期间安装,创建时机和目录占用就会变得复杂。因此它不是一般应用的首选方案。安全的目录权限和随机创建仍然更容易解释和维护。

PrivateTmp 是附加隔离,也会改变共享和清理语义

systemd 服务可以设置以下单元选项:

[Service]
PrivateTmp=yes

这是配置片段,不是完整服务文件。启用后,systemd 通过文件系统名称空间和绑定挂载,为该服务呈现独立的 /tmp/ 和 /var/tmp/。服务看到的路径不变,实际使用的是宿主机临时目录里的私有区域,减少了跨服务名称冲突和本地攻击面。

应用仍应使用安全的创建 API。某些容器环境不允许文件系统名称空间,不能假定这层隔离始终可用。另一方面,依赖两个服务通过 /tmp/ 传文件的设计会失效,因为两个服务看到的临时空间不同。启用该设置还会让这些私有临时目录随服务停止而清理;即使路径叫 /var/tmp/,也不应再据此推断内容跨服务重启存在。准备启用前,要核对应用是否使用临时路径进行进程间协作。

自动清理解决残留,也可能打断长任务

systemd-tmpfiles 会根据时间戳和配置实施老化清理,使异常退出、没有自行收尾的程序不至于无限堆积临时文件。原文列出的 /tmp/ 10 天、/var/tmp/ 30 天是其描述的默认策略,不能照搬为所有主机的承诺。管理员应检查本机合并后的 tmpfiles 配置、清理定时器及对应版本的手册。

这对长期任务提出一个问题:程序还认为文件有用,但路径已经被清理器移除。原文给出了七种策略,它们的适用对象和代价不同。

  1. 始终持有文件描述符,并通过它访问文件。文件从目录树中解除链接后,只要描述符还打开,进程仍能读写它。若不需要名称,可以创建后立即 unlink()。最后一个引用关闭后,内核负责回收资源;程序异常退出也会关闭其描述符。这保护的是已打开文件,不能让以后按原路径重新打开的操作继续成功。
  2. 直接创建匿名文件。memfd_create() 和 O_TMPFILE 省去了“先存在一个名称,再解除链接”的窗口。没有目录项,就没有按路径进行老化删除的对象。这尤其适合只需文件描述符的临时普通文件;不能拿它替代整个临时目录树。
  3. 对文件或目录持有 BSD 文件锁。flock() 的共享锁或排他锁可使 systemd-tmpfiles 的老化算法跳过被锁文件,或被锁目录及其子树。它适合需要多个文件、特殊文件节点或解包目录的任务。进程退出后锁随描述符关闭释放,清理器又能回收遗留内容。解压保留旧时间戳的 tar 包尤其需要留意:刚解出来的文件可能已符合老化条件。shell 可使用 util-linux 的 flock 工具表达这种生命周期。
  4. 定期更新时间戳。用 utimensat() 等接口维持文件访问时间,可以避免满足原文所述的老化条件。但这要求覆盖所有要保护的对象,正确设置更新频率,并理解实际清理器的判定。它比“资源存活等于描述符或锁存活”更容易遗漏。
  5. 普通文件使用 sticky bit。原文所述的 systemd-tmpfiles 逻辑跳过带 sticky bit 的普通文件。这条规则不适用于目录:目录上的 sticky bit 有不同的权限语义。不要把它误认为所有清理程序通用的“保留”标志。
  6. 选用应用自己的运行时或状态目录。特权服务可考虑 /run/ 下的专用目录,非特权应用可考虑 XDG_RUNTIME_DIR;需要持久数据则应按用途使用专用状态目录,例如 /var/lib/。原文也举到 ~/.config/,但不应因此把大块临时数据随意塞进用户配置目录。这类方案适合系统范围单实例、较长期状态的服务;程序要自行处理正常和异常退出的残留。/run/ 是运行时目录,不提供跨重启持久性。
  7. 通过 tmpfiles.d 排除。x、X 类型可按各自规则排除匹配路径的老化清理,也可使用 /run/tmpfiles.d/ 中的运行时配置。应查本机手册区分两种类型对目录和内容的处理。排除意味着放弃这层兜底,应用必须有可靠的回收办法,不能只添加一条永不清理的规则就结束。

原文优先推荐第二和第三种:只处理普通文件时,优先匿名文件;需要目录及多文件时,优先使用安全创建的目录并持有锁。这里关于锁和 sticky bit 的说法针对 systemd-tmpfiles 的相应清理语义,其他清理器可能不遵守。部署时应核对实际执行清理的软件。

空间不足要成为正常错误路径

公共临时目录共享容量,/tmp/ 常比持久磁盘空间紧张。匿名文件也会占内存、交换空间或文件系统容量,并不会消除资源耗尽风险。程序应限定临时数据规模,处理 ENOSPC,在错误消息中说明哪个位置没有空间,以及用户能采取什么措施。关键程序可以设计另一存放位置的回退,但回退时仍须维持权限、安全创建和容量限制。

版本校订:原文称 tmpfs 在内核中仍无原生配额支持,这一表述已经过时。当前 Linux 内核 tmpfs 文档列出 quota、usrquota、grpquota 及用户/组块数和 inode 硬限制等挂载选项,并说明相关选项不能在重新挂载时任意修改。实际是否可用仍取决于运行内核和挂载配置。不能从“tmpfs 支持配额”推断本机已开启配额,也不能从旧文推断它绝对不支持。

启动早期不要依赖常规临时目录

在 basic.target,更具体地说 local-fs.target 就绪前,/tmp/ 与 /var/tmp/ 可能尚未可用,或者不可写。启动早期组件更适合使用 memfd_create(),或 /run/ 下属于本程序的目录。

/dev/shm/ 也不是通用临时目录的替代品。它用于 POSIX 共享内存,通常同样由 tmpfs 支持,却是公共可写区域,也不提供本文讨论的通用自动清理保证。对程序而言,临时资源的位置、名称、打开的描述符、锁以及退出收尾应当组成同一个设计,而不是彼此独立的补丁。

核对日期:2026-10-05。本文的示意配置和 API 说明未执行;静态阅读不等于漏洞审计,也不证明目标主机的具体配置符合这些条件。配图由未完纪自绘。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容