如果我想将A
重命名为B
,但仅当B
不存在时,天真的事情就是检查B
是否存在(access("B", F_OK)
1}}或类似的东西),如果它没有继续rename
。不幸的是,这会打开一个窗口,在这个窗口期间,其他一些进程可能会决定创建B
,然后它会被覆盖 - 更糟糕的是,没有迹象表明发生了类似的事情。
其他文件系统访问功能不会受此影响 - open
有O_EXCL
(因此复制文件是安全的),最近Linux得到了一整套*at
系统调用保护对抗大多数其他种族条件 - 但不是这个特定的条件(renameat
存在,但可以防止完全不同的问题。)
它有解决方案吗?
答案 0 :(得分:15)
答案 1 :(得分:7)
您可以使用所需的新文件名link()
到现有文件,然后删除现有文件名。
link()
才能成功创建新链接。
类似的东西:
int result = link( "A", "B");
if (result != 0) {
// the link wasn't created for some reason (maybe because "B" already existed)
// handle the failure however appropriate...
return -1;
}
// at this point there are 2 filenames hardlinked to the contents of "A",
// filename "A" and filename "B"
// remove filename "A"
unlink( "A");
在link()
的文档中讨论了这种技术(请参阅有关修改passwd文件的讨论):
答案 2 :(得分:3)
很抱歉在旧帖子中添加了一些内容。并做了这么长的帖子。
我只知道在没有锁定的情况下完成一个完整的竞争条件rename()
的单一方法,它几乎可以在任何文件系统上运行,即使在具有间歇性服务器重启和客户端时间扭曲的NFS上也是如此。
以下配方是竞争条件免费,因为在任何情况下数据都不会丢失。它也不需要锁,可以由不想合作的客户端执行,除非他们都使用相同的算法。
从某种意义上说,如果事情严重破坏,一切都处于干净整洁的状态,这不是竞争条件。它也有很短的时间,源和目的地都不存在于它们的位置,但是源仍然是另一个名称。对于攻击者试图引发伤害的情况,它并没有变得强硬(rename()
是罪魁祸首,如图)。
S是源,D是目的地,P(x)是dirname(x)
,C(x,y)是x/y
路径连接
算法safe_rename(S,D)
解释:
问题在于我们要确保在源头和目的地都没有竞争条件。假设(几乎)每个步骤之间可能发生任何事情,但是在进行竞争条件自由重命名时,所有其他进程都遵循完全相同的算法。这包括永远不会触及临时目录T,除非确保(这是一个手动过程)使用该目录的进程已经死亡并且无法恢复(例如在恢复后继续VM休眠)。
要正确地执行rename()
,我们需要一些地方来隐藏。因此,我们构建一个目录,确保没有其他人(谁使用相同的算法)意外地使用它。
但是,mkdir()
并不保证在NFS上是原子的。因此,我们需要确保我们有一些保证我们在目录中独自一人。这是锁定文件中的O_EXCL
。这是 - 严格来说 - 不锁定,它是一个信号量。
除了极少数情况,mkdir()
通常是原子的。此外,我们可以为目录创建一些加密安全随机名称,添加一些GUID,主机名和PID,以确保其他人不太可能偶然选择相同的名称。但是为了证明算法是正确的,我们需要这个名为lock
的文件。
现在我们的目录基本上是空的,我们可以安全地rename()
来源。这可以确保在我们unlink()
之前,没有其他人改变来源。 (好吧,内容可以改变,这不是问题。)
现在可以应用link()
技巧以确保我们不会覆盖目的地。
之后unlink()
可以在其余来源上免费参加比赛。剩下的就是清理。
只剩下一个问题:
如果link()
失败,我们已经移动了源代码。为了正确清理,我们需要将其移回。这可以通过调用safe_rename(C(T,"tmp"),S)
来完成。如果这也失败了,我们所能做的就是尽可能多地尝试清理(unlink(C(T,"lock"))
,rmdir(T)
)并留下碎片以供管理员手动清理。
最后的说明:
为了帮助清理碎片盒,您可以使用比tmp
更好的文件名。巧妙地选择名称也可以使算法在一定程度上加强攻击。
如果您要在某处移动大量文件,您当然可以重复使用该目录。
但是,我同意,这种算法显然有点矫枉过正,而O_EXCL
上的rename()
类似缺失。
答案 3 :(得分:1)
从重命名手册页:
如果newpath已经存在,它将是 原子地取代(仅限于少数 条件;见下面的错误),这样 另一个没有意义 尝试访问newpath的进程 会发现它不见了。
因此,当B
文件已存在时,无法避免重命名。我想也许你别无选择,只能在你尝试重命名之前检查是否存在(使用stat()
而不是access()
),如果你不希望在文件已经存在时发生重命名。无视竞争条件。
否则,下面带有link()的解决方案似乎符合您的要求。
答案 4 :(得分:1)
从Linux内核3.15(2014年6月发布)开始,可以使用syscall(__ NR_renameat2,AT_FDCWD,“源文件”,AT_FDCWD,“目标文件”,RENAME_NOREPLACE)(包括<syscall.h>
, <fcntl.h>
和<linux/fs.h>
)。
这比link()更好,因为永远不会存在两个文件名同时存在的情况(特别是对于link()而言,定时断电可能会导致两个名字永远保留)。
glibc 2.28(于2018年8月发布)添加了一个renameat2()包装器,因此您可以使用它而不是syscall.h和linux / fs.h(尽管您很可能需要<stdio.h>
和{{ 1}})。
有关更多详细信息,请参见http://man7.org/linux/man-pages/man2/rename.2.html(尽管在撰写本文时确实如此,但不知道glibc现在具有一个renameat2包装器)。