如何重命名()没有竞争条件?

时间:2010-07-11 08:14:46

标签: linux unix race-condition

如果我想将A重命名为B,但仅当B不存在时,天真的事情就是检查B是否存在(access("B", F_OK) 1}}或类似的东西),如果它没有继续rename。不幸的是,这会打开一个窗口,在这个窗口期间,其他一些进程可能会决定创建B,然后它会被覆盖 - 更糟糕的是,没有迹象表明发生了类似的事情。

其他文件系统访问功能不会受此影响 - openO_EXCL(因此复制文件是安全的),最近Linux得到了一整套*at系统调用保护对抗大多数其他种族条件 - 但不是这个特定的条件(renameat存在,但可以防止完全不同的问题。)

它有解决方案吗?

5 个答案:

答案 0 :(得分:15)

您应该可以link(2)到新文件名。如果链接失败,则您放弃,因为该文件已存在。如果链接成功,则您的文件现在同时存在于旧名称和新名称下。然后你unlink(2)旧名。没有可能的竞争条件。

答案 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路径连接

  1. 检查目的地是否不存在。只是为了确保我们不会徒劳地采取下一步措施。
  2. 创建一个可能唯一的名称T:= C(P(D),随机)
  3. mkdir(T),如果此循环失败到上一步
  4. open(C(T,“lock”),O_EXCL),如果这失败rmdir(T)忽略错误并循环到上一步
  5. 重命名(S,C(T, “TMP”))
  6. 链路(C(T, “TMP”),d)
  7. 的unlink(C(T, “TMP”))
  8. 的unlink(C(T, “锁定”))
  9. 命令rmdir(T)
  10. 算法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包装器)。