9

如果我想重命名AB,但只有在B不存在的情况下,天真的事情会检查是否B存在(有access("B", F_OK)或类似的东西),如果它不继续rename. 不幸的是,这会打开一个窗口,在此期间其他进程可能会决定创建B,然后它会被覆盖——更糟糕的是,没有迹象表明曾经发生过类似的事情。

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

那么它有解决方案吗?

4

5 回答 5

15

您应该能够将 (2) 链接到新文件名。如果链接失败,那么您放弃,因为该文件已经存在。如果链接成功,您的文件现在以旧名称和新名称存在。然后您取消链接(2) 旧名称。没有可能的竞争条件。

于 2010-07-11T09:00:48.897 回答
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 文件的讨论):

于 2010-07-11T09:04:00.640 回答
3

很抱歉在旧线程中添加了一些东西。并且做了这么长的帖子。

我只知道一种方法可以rename()在没有锁定的情况下免费完成完整的竞争条件,这几乎可以在任何文件系统上工作,即使在 NFS 上,服务器间歇性重启和客户端时间扭曲到位。

以下配方是无竞争条件的,因为在任何情况下数据都不会丢失。它也不需要锁,并且可以由不想合作的客户端执行,除非它们都使用相同的算法。

从某种意义上说,如果某些东西严重损坏,一切都会保持干净整洁的状态,这不是没有竞争条件的。它也有一段很短的时间,源头和目的地都不在他们的位置,但是源头仍然以另一个名字存在。并且对于攻击者试图引起伤害的情况(这rename()是罪魁祸首,去想),它并没有得到强化。

S 是源,D 是目的地,P(x) 是dirname(x),C(x,y) 是x/y路径连接

  1. 检查目的地不存在。只是为了确保我们不会徒劳地执行下一步。
  2. 创建一个可能唯一的名称 T := C(P(D),random)
  3. mkdir(T),如果这失败循环到上一步
  4. open(C(T,"lock"),O_EXCL),如果失败 rmdir(T) 忽略错误并循环到上一步
  5. 重命名(S,C(T,“tmp”))
  6. 链接(C(T,“tmp”),D)
  7. 取消链接(C(T,“tmp”))
  8. 取消链接(C(T,“锁定”))
  9. rmdir(T)

算法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_EXCLon的东西。rename()

于 2011-03-19T01:21:05.273 回答
2

从 Linux 内核 3.15(2014 年 6 月发布)开始,这可以通过 syscall(__NR_renameat2, AT_FDCWD, "source-file", AT_FDCWD, "dest-file", RENAME_NOREPLACE) (包括和<syscall.h>)来完成。<fcntl.h><linux/fs.h>

这比 link() 好,因为永远不会有两个文件名同时存在的点(特别是使用 link(),精确定时的断电可能会导致两个文件名永久保留)。

glibc 2.28(2018 年 8 月发布)添加了 renameat2() 包装器,因此您可以使用它来代替 syscall.h 和 linux/fs.h(尽管您很可能需要<stdio.h>and#define __GNU_SOURCE来代替)。

有关更多详细信息,请参阅http://man7.org/linux/man-pages/man2/rename.2.html(尽管在撰写本文时确实如此,但不知道 glibc 现在有 renameat2 包装器)。

于 2018-08-01T22:47:58.707 回答
1

从重命名手册页:

如果 newpath 已经存在,它将被原子地替换(受一些条件的影响;请参阅下面的错误),以便尝试访问 newpath 的另一个进程不会发现它丢失。

B因此,当文件已经存在时,无法避免重命名。我认为,如果您不希望在文件已存在的情况下进行重命名,那么您可能别无选择,只能在尝试重命名之前检查是否存在(stat()不使用)。access()忽略竞争条件。

否则,下面提供的带有 link() 的解决方案似乎符合您的要求。

于 2010-07-11T08:21:28.920 回答