LWN:特别的组合O_DIRECTORY|O_CREATE

关注了就能看到更多这么棒的文章哦~

The curious case of O_DIRECTORY|O_CREAT

By Jonathan Corbet
March 27, 2023
DeepL assisted translation
https://lwn.net/Articles/926782/

open() 系统调用提供了一些 flag,可以改变其行为。在一次系统调用中可以组合这些 flag,但是并不是所有组合都有意义。然而,事实证明,内核对 O_CREAT 和 O_DIRECTORY 的组合在很长时间以来一直按照某种令人惊讶的方式来实现的。在 2020 年的一个改动使得这种反应更加出人意料了。看起来这种行为很快就会被 fix,进而会导致一个核心系统调用出现了很少见的用户可见的语义变化的情况。

O_CREAT 标志要求 open()在指定路径不存在的情况下创建一个普通文件(增加 O_EXCL 将导致调用在文件确实存在的情况下报错)。而 O_DIRECTORY 则表示只有当路径存在并且是一个目录时,该调用才会成功。用 open() 不可能创建一个目录;那毕竟是 mkdir()的才该做的事情。所以 O_CREAT 和 O_DIRECTORY 组合起来用的话就是要求内核将一个目录(本来就已经存在)作为一个普通文件来创建出来,这显然是不合理的。

一直以来,内核对这两个 flag 组合的情况下通常会报错。如果该路径存在并且是一个普通的文件,open()就会失败并返回一个 ENOTDIR 错误。相反,如果路径是一个本已存在的目录的话,该错误就是 EISDIR,这也许有点让人出乎意料,因为 O_DIRECTORY 表明本来就期望该路径是一个目录。然而,如果制定路径根本不存在的话,open()调用将在创建一个指定名称的普通文件后成功,这也是一个让人意外的结果。

不过最近,Pedro Falcato 注意到上述最后一种情况下的行为发生了变化:如果路径不存在,内核现在会返回 ENOTDIR,但它也仍然会创建一个普通文件。可以说,这种行为比之前的处理更令人惊讶。Christian Brauner 追踪了一下这个行为是怎么变动的,找到了 Al Viro 提交的一个 commit,其被被合并到了 5.7 版本中。

Falcato 包含了一个 patch 来恢复到以前的行为,可以说这比内核现在的做法更有意义,无论如何,内核在很长一段时间内都是这样做的。但是 Brauner 不确定这里正确的做法是不是应该 fix 内核代码,使其对这种组合情况下的动作更加合理:

所以,在我们继续这个方向之前,我们是否应该把这当作一个修复旧的 bug 的机会?因为这种返回 -ENOTDIR 的行为从 v5.7 开始就存在了。从那时起,我们有三个 LTS 版本都返回 ENOTDIR,即使是在文件已经被创建了的情况下。

他说,既然似乎没有人注意到这段时间的改动,那么就很可能没有人真正在使用过去这个组合的奇怪语义。Linus Torvalds 表示同意,实际对 kernel 行为进行 fix 似乎就是一条明智的道路了: "我认为我们可以假设没有实际的用户,我们不妨适当地清理一下语义"。

Falcato 做了一些研究,看看其他系统对这种标志组合做了什么反应。NetBSD,似乎在这种情况下会简单地拒绝 open()调用,返回 EINVAL。而 FreeBSD,如果路径存在并且是一个目录的话就会允许调用成功;否则将失败。他还指出,所有这些各种行为,包括 5.7 之前和之后的 Linux 的行为、NetBSD 和 FreeBSD 的行为,都是 POSIX 所允许的:"我不会把古老的 Linux 的行为称为 *bug*,只是真的是很奇怪的语义"。

Torvalds 回答说,这两种 BSD 方案都是合理的,而内核目前的行为 "没有什么借口"。他说,NetBSD 的应对是 "最干净的处理",但是 FreeBSD 的行为更接近于 Linux 在 5.7 修改之前的行为。Brauner 倾向于 NetBSD 的行为,并实现了一个 patch。作为这项工作的一部分,他花了一些精力在代码中寻找会因语义变化而被导致行为变化的情况;他几乎一无所获:

花了很多时间来寻找这个组合的潜在用户。在 codesearch.debian.net 上的搜索显示,代码库经常表现出一些关于 O_DIRECTORY | O_CREAT 组合的语义上的行为期望,这与我们的代码过去和现在的做法完全相反。

这些期望通常是利用这个特定的组合来创建并打开一个目录。这表明,无论在 v5.7 之前还是 v5.7 之后,试图使用这种组合的用户都应该会偶然发现这种反常的行为,并很快意识到这两种语义都不能满足他们的需要。

patch 中包含了一些开发者尝试这种组合的链接,可以看看 libglnx 的 comment 作为一个例子。

在 Brauner 的 patch 合入之后,O_CREAT 和 O_DIRECTORY 的组合将会导致 open()调用失败,报出 EINVAL 错误,无论给定的路径是否存在。这个改动有可能不会造成任何破坏,但他要求进行广泛的测试来确定这一点。毕竟,如果在未来的某个时间点报出 bug 的话,就不得不 revert 这个改动,那会是很让人烦恼的。截至目前,这个 patch 实际上还没有被合入;考虑到涉及到语义上的变化,估计在 6.3 中应该不会合入了。不过编者以前也碰到过类似的情况被合入了。

这是内核的 API 政策中的一个很微妙的例子。在真正意义上来说,这个 fix 是导致了 API 不兼容的一个改动,它确实会破坏任何依赖当前行为的程序。但是,在没有程序依赖这个特定行为的情况下,该行为确实可以被修改。这种 fix 似乎不太可能破坏任何东西,所以内核开发者是可以做这个改动的。如果 "不会破坏任何东西" 的假设被证明是正确的,那么有一天,甚至有可能让这组 flag 组合来真正被开发人员利用起来做他们原本期望的事情,并创建一个目录。但是,首先 bixu 证明删除当前的奇怪语义确实不会产生任何问题。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

format,png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值