使用fseek和ftell确定文件大小存在漏洞?

14

我读过一些帖子,展示了如何使用fseek和ftell来确定文件的大小。

FILE *fp;
long file_size;
char *buffer;

fp = fopen("foo.bin", "r");
if (NULL == fp) {
 /* Handle Error */
}

if (fseek(fp, 0 , SEEK_END) != 0) {
  /* Handle Error */
}

file_size = ftell(fp);
buffer = (char*)malloc(file_size);
if (NULL == buffer){
  /* handle error */
}

我本打算使用这种技术,但是后来遇到了这个链接,其中描述了潜在的漏洞。

该链接建议使用fstat而不是使用fseek和ftell。请问有人能发表评论吗?

5个回答

17

这个链接是来自CERT的众多毫无意义的C编码建议之一。他们的理由基于C标准允许实现所采取的自由,但在POSIX中不被允许,因此在你有fstat作为替代方案的所有情况下都是无关紧要的。

POSIX要求:

  1. fopen"b"修饰符没有任何效果,即文本和二进制模式的行为相同。这意味着他们对于在文本文件上调用UB的担忧是无稽之谈。

  2. 文件大小由写操作和截断操作设置为字节分辨率大小。这意味着他们对文件末尾随机数量的空字节的担忧也是无稽之谈。

可悲的是,他们发表的所有这样的胡言乱语使人难以知道哪些CERT出版物应该认真对待。这很遗憾,因为他们的许多建议都是严肃的。


1
就使用fseek来确定文件大小的建议会提高安全性这一想法,我表示同意。如果我想知道文件的大小,我仍然会使用fstat,但这是因为我认为它干净明了,而不是因为我认为它本身可以避免安全问题。 - John Zwinck
2
使用 fseek 的优点是它可以在块设备文件上工作。 - bdonlan
1
我会避免使用 fstat,因为它会排除在设备节点上操作的可能性,而你的应用程序在其他情况下可能正常工作。 - R.. GitHub STOP HELPING ICE
他们为什么希望以相同的方式读取文本和二进制数据?!这太愚蠢了! - MarcusJ
3
@MarcusJ:我不确定你在问什么或者你的评论对于这个问题或答案有什么贡献。 - R.. GitHub STOP HELPING ICE
1
基本上,POSIX 表示文本和二进制文件被视为相同的,因此在 POSIX 系统上,行尾字符始终只有一个字符。但是在 Windows 上,它不是一个字符,因此 C 标准需要同时支持 b 和 r 文件模式。@MarcusJ - Jerry Jeremiah

8
如果你的目标是找到文件的大小,那么一定要使用fstat()或其相关函数。这是一种更直接和表达力更强的方法——你实际上是在向系统询问文件的统计信息,而不是通过更迂回的fseek/ftell方法。
额外的提示:如果你只想知道文件是否可用,请使用access()而不是打开文件甚至进行stat操作。这是一个更简单的操作,许多程序员都不知道。

3
这取决于 POSIX 功能(fstat),而不是普通的 C,它不够可靠。例如,如果“文件”是 /dev/sda1fstat 将显示大小为0,但 fseek/ftell 方法将获取分区的大小。(实际上,你应该使用使用 off_tfseeko/ftello,因为 long 可能太短而无法存储分区的大小...) - R.. GitHub STOP HELPING ICE
如果目标是确定文件的大小以便分配足够大的缓冲区,那么有什么区别吗? - Frank
1
有很多情况下,你需要读取整个文件。首先想到的就是 sort - R.. GitHub STOP HELPING ICE
你认为 sort 会分配一个大小与输入文件相同的巨大缓冲区吗?我不确定,但我会有点惊讶。 - John Zwinck
2
我刚试了一下。它会分配一个缓冲区,大小为最长行的长度,但在此之后,它使用临时文件来存储排序时的行。它似乎没有为整个输入文件分配一个巨大的区域。 - John Zwinck
显示剩余3条评论

5
我倾向于同意他们的基本结论,即通常不应该直接在代码主流中使用fseek / ftell代码--但你也许不应该使用fstat。如果您想要文件的大小,大多数代码应该使用像filesize这样具有明确,直接名称的东西。
现在,最好是在可用的情况下使用fstat来实现它,并且(例如)在Windows上使用FindFirstFile(当fstat通常不可用时最明显的平台)。
故事的另一面是,与二进制文件相关的许多(大多数?)对fseek的限制实际上起源于CP/M,它在任何地方都没有明确存储文件的大小。文本文件的结尾由控制-Z表示。但是对于二进制文件,你真正知道的只是用于存储文件的扇区。在最后一个扇区中,您有一些未使用的数据,通常(但并非总是)为零填充。不幸的是,可能会有一些零是重要的,或者非重要的非零值。
如果整个C标准在获得批准之前刚被写出(例如,如果它在1988年开始,在1989年完成),他们可能完全忽略了CP/M。然而,他们在1982年左右开始制定C标准,当时CP/M仍被广泛使用,因此它不能被忽视。到CP/M消失的时候,许多决策已经做出,我怀疑任何人都不想重新审视它们。
然而,对于大多数人来说,这没有意义--大多数代码无法在没有大量工作的情况下移植到CP/M;这是要处理的相对较小的问题之一。使现代程序在48K(左右)的内存中运行以及数据是一个更为严重的问题(仅具有约一兆字节的大量存储也将是另一个严重问题)。
但CERT确实有一个好处:您可能不应该(通常这样做)找到文件的大小,分配那么多空间,然后假设文件的内容将适合其中。即使fseek / ftell在现代系统中可以给出正确的大小,但是该数据在您实际读取数据时可能已过时,因此您仍然可能会溢出缓冲区。

3
就你最后一段所说的,只要你在为缓冲区分配长度和读取缓冲区时使用相同的长度,就不会有溢出危险。而且无法原子性地读取文件,因此即使可能出现长度过期的情况,但这种情况是不可避免的。 - R.. GitHub STOP HELPING ICE
@R:如果你计算了一个长度,然后假设整个文件都能适应,那么问题就出现了。为了避免长度过时,你只需读取到文件末尾,如果必要的话扩展缓冲区(或使用类似mmap的东西...)。 - Jerry Coffin
你的解决方法只适用于对文件进行追加操作时。而且,mmap 更加危险。如果在 mmap 后文件大小减小,读取时会触发 SIGBUS 错误,并且无法安全恢复。 - R.. GitHub STOP HELPING ICE
5
@R:你显然缺乏想象力!p=malloc(len); while ((ch=getc())!=EOF) *p++=ch;当然,我有一个小优势:我不必想象——我至少修复了那么糟糕的代码。 - Jerry Coffin
1
@VioletGiraffe:这有点取决于情况。在某些情况下,如果文件太大,您甚至不想尝试读取该文件,但是GetFileSizeEx不支持此操作——它要求您在获取大小之前打开文件(此时,即使您最终没有读取文件,也已更改了文件的上次访问时间)。因此,如果您在不知道如何使用它的所有详细信息的情况下盲目地推荐一个函数,则FindFirstFile是较少侵入性的(因此更广泛适用)。 - Jerry Coffin
显示剩余3条评论

4

不使用fstat的原因在于fstat是POSIX标准,而fopenftellfseek是C标准的一部分。

可能存在一种实现C标准但不支持POSIX的系统。在这样的系统上,fstat将无法工作。


5
CERT咨询的重点是,在这样的系统上,寻找/告诉方法可能具有实现定义或未定义的行为。然而,在这样的系统上,您没有fstat。任何具有fstat的系统肯定具有明确、定义良好的文件偏移和长度概念。 - R.. GitHub STOP HELPING ICE
C语言被用于一些没有操作系统的系统上。因此,除非你知道谁将运行你的代码,否则你不能假设它符合POSIX标准。即使Windows也不是POSIX标准,尽管它有一个fstat函数(但该函数被称为_fstat,因为Microsoft希望你知道它不是标准函数)。 - Jerry Jeremiah

2
根据C标准,§7.21.3
设置文件位置指示器为文件结尾,如fseek(file,0,SEEK_END),对于二进制流(由于可能存在尾随的空字符)或任何具有状态相关编码且不确定以初始换转状态结束的流都具有未定义行为。
一个拘泥于法律字面意思的人可能认为,可以通过计算文件大小来避免这种UB:
fseek(file, -1, SEEK_END);
size = ftell(file) + 1;

但是C标准也这样说:
二进制流不必支持带 SEEK_END 参数的 fseek 调用。
因此,对于 fseek/SEEK_END,我们无法采取措施来修复它。尽管如此,我仍然更喜欢使用 fseek/ftell 而不是特定于操作系统的 API 调用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接