【问题标题】:Why is this not a bug in qmail?为什么这不是 qmail 中的错误?
【发布时间】:2010-04-03 06:11:08
【问题描述】:

我正在阅读 DJB 的 "Some thoughts on security after ten years of Qmail 1.0",他列出了这个用于移动文件描述符的函数:

int fd_move(到,从) 诠释为; 来自; { 如果(到 == 从)返回 0; if (fd_copy(to,from) == -1) 返回 -1; 关闭(从); 返回0; }

我突然想到这段代码没有检查 close 的返回值,所以我阅读了 close(2) 的手册页,似乎 can 失败并出现EINTR,在在这种情况下,适当的行为似乎是使用相同的参数再次调用 close。

由于这段代码是由在 C 和 UNIX 方面比我有更多经验的人编写的,而且在 qmail 中十多年来一直保持不变,我认为我认为一定有一些细微的差别导致了这段代码正确的。谁能向我解释一下这种细微差别?

【问题讨论】:

  • 可能在调用函数 fd_mode 之前检查了“from”
  • 以什么方式可以检查“从”以防止从 close(2) 返回 EINTR?

标签: c unix qmail


【解决方案1】:

我有两个答案:

  1. 他试图说明关于分解通用代码的观点,并且为了简洁明了,这些示例通常会省略错误检查。
  2. close(2) 可能会返回 EINTER,但实际上会返回,如果是,您会合理地做什么?重试一次?重试直到成功?如果你得到 EIO 怎么办?这可能意味着几乎任何事情,所以除了记录它并继续前进之外,你真的没有合理的追索权。如果您在 EIO 之后重试,您可能会得到 EBADF,然后呢?假设描述符已关闭并继续前进?

每个系统调用都可以返回 EINTR,尤其是像 read(2) 这样阻塞等待慢人的系统调用。这是一个更有可能的情况,一个好的“从终端获取输入”例程确实会检查这一点。这也意味着 write(2) 可能会失败,即使在写入日志文件时也是如此。您是尝试记录记录器生成的错误还是应该放弃?

【讨论】:

  • 我只是快速浏览了一下 linux2.6/fs/open.c 内核代码,而 man 2 close 和所有系统调用一样有样板 EINTR 返回,看起来 sys_close 可能只有每个返回EBADF。
  • "如果是这样,你会合理地做什么?"通常重试直到成功(如果合适,首先处理信号)。如果你得到 EIO 或 EBADF,你就放弃了,因为这些都是与文件描述符状态有关的永久性故障。 EINTR 是与进程/线程状态有关的临时故障。至少,如果你的程序在其他方面是正确的。如果您对close 的调用导致 信号被发送到您的进程,那么就大错特错了。
  • @Steve,重试直到成功可能会导致程序陷入无限循环。最好假设它已成功关闭(让描述符保持打开状态,以防万一失败)。
  • @msw,第三种选择是作者假设将使用“sigaction”安装任何信号处理程序,并指定“SA_RESTART”标志(重新启动中断的系统调用)。
  • @msw 复制的代码是 qmail 1.03 的逐字记录,所以它不仅仅是示例代码。
【解决方案2】:

当一个文件描述符被复制时,就像它在fd_copydup2 函数中一样,你最终会得到多个文件描述符引用相同的东西(即相同的struct file 在核心)。关闭其中一个只会减少其引用计数。除非是 last 关闭,否则不会对基础对象执行任何操作。因此,EINTREIO 这样的条件是不可能的。

【讨论】:

  • 但是在 close(2) 系统调用中信号被阻塞了吗?如果不是,则可以在任意两个指令之间的任何时间点传递信号——甚至在引用计数的递减和系统调用的返回之间,对吧?
  • 如果信号在线程处于系统调用时到达,则信号将处于挂起状态,直到系统调用进入可中断睡眠或系统调用返回。系统调用可能会被另一个线程/中断处理程序不自觉地抢占,但是当被中断的线程恢复时,它将从系统调用中断的地方继续,并且不会在该点检查挂起的信号。系统调用不能在任意指令处中止;不知道可能会使线程处于什么状态。
  • 这种行为在 Linux 上可能就是这种情况,但如果不使用 SA_RESTART 和 sigaction,它就无法移植。
【解决方案3】:

另一种可能性是,他的函数仅在应用程序(或其中的一部分)中使用,该应用程序已采取措施确保调用不会被信号中断。如果您不打算对信号做任何重要的事情,那么您不必对它们做出响应,并且将它们全部屏蔽掉可能是有意义的,而不是将每个阻塞系统调用包装在 EINTR 重试中。当然除了会杀死你的那些,所以如果你通过退出来处理它,那么 SIGKILL 和经常 SIGPIPE 以及 SIGSEGV 和类似的致命错误在任何情况下都不会传递到正确的用户空间应用程序。

无论如何,如果他所说的只是安全性,那么他很可能不必重试close。如果关闭 EIO 失败,那么他将无法重试,这将是永久失败。因此,close 成功对于他的程序的正确性是不必要的。很可能他的程序的正确性也没有必要在 EINTR 上重试close

通常您希望您的程序尽最大努力取得成功,这意味着在 EINTR 上重试。但这是与安全性不同的问题。如果您的程序设计为使某些功能因任何原因而失败不是安全漏洞,那么特别是它碰巧 EINTR 失败的事实,而不是永久原因,不是一个缺陷。众所周知,DJB 相当固执己见,所以如果他证明了他不需要重试的某种原因,我一点也不感到惊讶,因此即使这样做也不会打扰将允许他的程序在当前可能失败的某些情况下成功刷新句柄(例如在关键时刻由用户显式发送带有kill 的无害信号)。

编辑:在我看来,在某些情况下重试 EINTR 本身可能是一个安全漏洞。它为该部分代码引入了一种新行为:它可以无限循环以响应信号泛滥,以前它会尝试close然后返回。我不确定这是否会导致 qmail 出现任何问题(毕竟,close 本身并不能保证它会在多长时间内返回)。但是,如果在一次尝试后放弃确实使代码更易于分析,那么它可能是一个明智的举措。或者不。

您可能认为重试可以防止 DoS 缺陷,即信号会导致虚假故障。但是重试允许另一个(更困难的)DoS 缺陷,其中信号泛滥会导致无限期的停顿。就二进制“这个应用程序可以 DoSed 吗?”而言,这是 DJB 在编写 qmail 和 djbdns 时感兴趣的绝对安全问题,没有区别。如果某件事可以发生一次,那么通常这意味着它可以发生多次。

【讨论】:

    【解决方案4】:

    只有在没有你明确要求的情况下才会返回 EINTRsignal() 的健全语义支持可重新启动的系统调用(“BSD 风格”)。在使用 sysv 语义(中断信号)的系统上构建程序时,您应该始终将对 signal() 的调用替换为对 bsd_signal() 的调用,如果它不存在,您可以根据 sigaction() 定义自己。

    进一步值得注意的是,no 系统将在收到信号时返回 EINTR,除非您安装了信号处理程序。如果保留默认操作,或者将信号设置为无操作,则系统调用不可能被中断。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-07-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-02-05
      • 2018-08-18
      相关资源
      最近更新 更多