为什么gets功能如此危险以至不应该使用它?


229

当我尝试gets()通过GCC 编译使用该函数的C代码时,收到以下警告:

(.text + 0x34):警告:“ gets”功能很危险,不应使用。

我记得这与堆栈保护和安全性有关,但是我不确定为什么。

如何删除此警告,为什么会有这样的使用警告gets()

如果gets()是如此危险,那我们为什么不能将其删除?



Answers:


179

为了gets安全使用,您必须确切地知道将要读取多少个字符,以便可以使缓冲区足够大。您只会知道,如果您确切知道将要读取的数据。

gets您要使用而不是使用fgets,它具有签名

char* fgets(char *string, int length, FILE * stream);

fgets,如果它读取整行,则将保留'\n'在字符串中;您必须处理该问题。)

直到1999 ISO C标准,它仍然是该语言的正式组成部分,但2011年标准正式将其删除。大多数C实现仍然支持它,但是至少gcc对使用它的任何代码发出警告。


79
警告实际上不是gcc,而是包含编译指示或属性的glibc,gets()使用时会导致编译器发出警告。
2015年

@fuz实际上,警告的不仅是编译器:OP引用的警告是由链接程序打印的!
Ruslan

163

为什么gets()危险

第一个Internet蠕虫(Morris Internet Worm)大约在30年前(1988-11-02)逃脱了,它使用gets()缓冲区溢出作为其在系统之间传播的方法之一。基本问题是该函数不知道缓冲区有多大,因此它将继续读取直到找到换行符或遇到EOF为止,并且可能会溢出给定缓冲区的范围。

您应该忘记曾经听说过的gets()存在。

取消gets()了C11标准ISO / IEC 9899:2011 的标准功能,即A Good Thing™(在ISO / IEC 9899:1999 / Cor.3:2007 —技术勘误中正式标记为“过时”和“已弃用”) 3表示C99,然后在C11中删除)。可悲的是,由于向后兼容的原因,它将在库中保留很多年(意思是“十年”)。如果由我决定,的实现gets()将变为:

char *gets(char *buffer)
{
    assert(buffer != 0);
    abort();
    return 0;
}

鉴于您的代码迟早会崩溃,因此最好早点解决此问题,而不是稍后再解决。我准备添加一条错误消息:

fputs("obsolete and dangerous function gets() called\n", stderr);

如果您进行链接,则现代版本的Linux编译系统会生成警告gets(),并且还会针对某些也存在安全问题的其他功能(mktemp(),…)发出警告。

替代品 gets()

fgets()

就像其他人所说的,规范的替代选择gets()fgets()指定stdin为文件流。

char buffer[BUFSIZ];

while (fgets(buffer, sizeof(buffer), stdin) != 0)
{
    ...process line of data...
}

还没有人提到的是gets()不包括换行符,而是包含换行符fgets()。因此,您可能需要使用包装器fgets()来删除换行符:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
    if (fgets(buffer, buflen, fp) != 0)
    {
        size_t len = strlen(buffer);
        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        return buffer;
    }
    return 0;
}

或更好:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
    if (fgets(buffer, buflen, fp) != 0)
    {
        buffer[strcspn(buffer, "\n")] = '\0';
        return buffer;
    }
    return 0;
}

此外,如咖啡厅指出,在评论和paxdiablo显示在他的回答,跟fgets()你可能有数据遗留在一条线上。我的包装器代码使该数据下次可以读取;您可以根据需要随时对其进行修改,以吞噬其余数据行:

        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        else
        {
             int ch;
             while ((ch = getc(fp)) != EOF && ch != '\n')
                 ;
        }

剩余的问题是如何报告三种不同的结果状态-EOF或错误,行读取但未截断,部分行读取但数据被截断。

gets()之所以不会出现此问题,是因为它不知道缓冲区的结束位置,并且会在缓冲区的尽头践踏,对漂亮的内存布局造成严重破坏,如果在缓冲区上分配了缓冲区,则经常弄乱返回堆栈(Stack Overflow)堆栈,或者如果动态分配了缓冲区,则践踏控制信息;如果静态分配了缓冲区,则通过其他宝贵的全局(或模块)变量复制数据。这些都不是一个好主意-它们概括了“未定义的行为”一词。


还有TR 24731-1(C标准委员会的技术报告),它为各种功能提供了更安全的替代方案,其中包括gets()

§6.5.4.1 gets_s功能

概要

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

运行时约束

s不得为空指针。n既不等于零也不大于RSIZE_MAX。n-1从中读取字符时会出现换行符,文件结尾或读取错误 stdin25)

3如果存在运行时约束冲突,s[0]则将其设置为空字符,并从stdin开始读取和丢弃字符开始,直到读取新行字符,文件结束或读取错误。

描述

4该gets_s函数n 从所指向的流中读取最多小于所指定的字符数的1 stdin到所指向的数组中s。在换行符(已被丢弃)之后或文件结尾之后,不会再读取其他字符。丢弃的换行符不计入读取的字符数。在将最后一个字符读入数组后,立即写入空字符。

5如果遇到文件结尾并且没有字符读入数组,或者在操作过程中发生读取错误,则将s[0]其设置为空字符,而其他元素则s采用未指定的值。

推荐做法

6该fgets函数允许正确编写的程序安全地处理过长的输入行,以至于无法存储在结果数组中。通常,这要求调用者fgets注意结果数组中是否存在换行符。考虑使用fgets(以及基于换行符的所有必要处理)代替 gets_s

25)gets_s函数不同于gets,它使输入行溢出缓冲区以存储它,从而违反了运行时约束。不同于fgetsgets_s在输入行和对的成功调用之间保持一对一的关系gets_s。使用的程序gets期望这种关系。

Microsoft Visual Studio编译器实现了对TR 24731-1标准的近似,但是Microsoft与TR中的签名之间存在差异。

C11标准ISO / IEC 9899-2011将TR24731包含在附件K中,作为库的可选部分。不幸的是,它很少在类Unix系统上实现。


getline() — POSIX

POSIX 2008还提供了一个安全的替代gets()getline()。它为该行动态分配空间,因此最终需要释放它。因此,它消除了对线长的限制。它还返回读取的数据的长度,或者-1(不是EOF!),这意味着可以可靠地处理输入中的空字节。还有一个称为“选择您自己的单字符定界符”的变体getdelim();例如,如果要处理find -print0文件名末尾用ASCII NUL '\0'字符标记的输出,这将很有用。


8
还值得指出的是fgets(),您的fgets_wrapper()版本将在输入缓冲区中留下超长行的尾部,以供下一个输入函数读取。在许多情况下,您需要读取和丢弃这些字符。
caf 2010年

5
我想知道为什么他们没有添加fgets()替代方案,该替代方案使人们无需进行愚蠢的调用就可以使用其功能。例如,一个fgets变体返回读取到字符串中的字节数,这将使代码易于查看最后读取的字节是否为换行符。如果将为缓冲区传递空指针的行为定义为“读取并丢弃n-1个字节,直到下一个换行符”,这将使代码轻松丢弃超长行的尾部。
超级猫

2
@supercat:是的,我同意-很遗憾。最接近的方法可能是POSIX getline()及其相对方法getdelim(),它们确实返回命令读取的“行”的长度,并根据需要分配空间以能够存储整行。如果最终得到的单行JSON文件大小为数GB,则即使这样做也会引起问题。你能承受所有的记忆吗?(当我们这样做时,是否可以有strcpy()strcat()变体在最后返回一个指向空字节的指针?等等)
乔纳森·莱弗勒

4
@supercat:另一个问题fgets()是,如果文件包含一个空字节,则无法确定从空字节到行尾(或EOF)的末尾有多少数据。 strlen()最多只能报告数据中的空字节;在那之后,这是猜测,因此几乎可以肯定是错误的。
乔纳森·勒夫勒

7
“忘记你曾经听说过的gets()存在。” 当我这样做时,我会再次遇到它并回到这里。您是否正在入侵stackoverflow以获得投票?
candied_orange

21

因为gets在从stdin获取字节并将它们放在某处时不进行任何类型的检查。一个简单的例子:

char array1[] = "12345";
char array2[] = "67890";

gets(array1);

现在,首先您可以输入想要的字符数,gets而不用担心。其次,超过放置它们的数组大小的字节(在这种情况下为array1)将覆盖它们在内存中找到的所有内容,因为gets将对其进行写入。在前面的示例中,这意味着,如果您输入的内容"abcdefghijklmnopqrts"可能无法预料,它将覆盖array2所有内容。

该函数不安全,因为它假定输入一致。永远不要使用它!


3
是什么让gets彻底无法使用的是它不具有数组长度/计数参数,它需要; 如果它在那里,那将是另一个普通的C标准函数。
legends2k 2013年

@ legends2k:我很好奇预期的用途gets是什么,为什么没有标准的fgets变体在不需要换行作为输入一部分的用例中变得如此方便?
超级猫

1
gets顾名思义,@supercat 旨在从中获取字符串stdin,但是不具有size参数的基本原理可能来自C的精神:信任程序员。C11中删除了此功能,并且给定的替换gets_s占用了输入缓冲区的大小。我对这fgets部分一无所知。
legends2k

@ legends2k:我可以看到的唯一可辩解的上下文gets是,如果使用的是硬件行缓冲的I / O系统,该系统实际上无法提交一定长度的行,并且程序的预期寿命是比硬件的寿命短。在这种情况下,如果硬件无法提交超过127个字节的行,则可以将其提交gets到128字节的缓冲区中是合理的,尽管我认为当期望较小的输入时能够指定较短的缓冲区的好处远比证明行之有效。成本。
超级猫

@ legends2k:实际上,理想的情况是让“字符串指针”标识一个字节,该字节将在几种不同的字符串/缓冲区/缓冲区信息格式中进行选择,其中一个前缀字节的值指示一个包含以下内容的结构前缀字节[加上填充],再加上缓冲区大小,使用的大小和实际文本的地址。这样的模式将使代码可以传递另一个字符串的任意子字符串(而不仅仅是尾部)而不必复制任何内容,并且将允许类似的方法getsstrcat安全地接受尽可能多的方法。
超级猫

16

您不应该使用gets它,因为它无法停止缓冲区溢出。如果用户输入的数据超出缓冲区的容量,则很可能导致损坏甚至更糟。

实际上,ISO实际上已经采取了从C标准中删除 的步骤(gets从C11开始,尽管在C99中已弃用),考虑到它们向后兼容的程度,这应该表明该功能有多糟糕。

正确的做法是将fgets函数与stdin文件句柄一起使用,因为可以限制从用户读取的字符。

但这还存在以下问题:

  • 用户输入的多余字符将在下一次提取。
  • 没有快速通知用户输入了太多数据。

为此,几乎每个C程序员在其职业生涯中的某个时候都将编写一个更有用的包装器fgets。这是我的:

#include <stdio.h>
#include <string.h>

#define OK       0
#define NO_INPUT 1
#define TOO_LONG 2
static int getLine (char *prmpt, char *buff, size_t sz) {
    int ch, extra;

    // Get line with buffer overrun protection.
    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }
    if (fgets (buff, sz, stdin) == NULL)
        return NO_INPUT;

    // If it was too long, there'll be no newline. In that case, we flush
    // to end of line so that excess doesn't affect the next call.
    if (buff[strlen(buff)-1] != '\n') {
        extra = 0;
        while (((ch = getchar()) != '\n') && (ch != EOF))
            extra = 1;
        return (extra == 1) ? TOO_LONG : OK;
    }

    // Otherwise remove newline and give string back to caller.
    buff[strlen(buff)-1] = '\0';
    return OK;
}

带有一些测试代码:

// Test program for getLine().

int main (void) {
    int rc;
    char buff[10];

    rc = getLine ("Enter string> ", buff, sizeof(buff));
    if (rc == NO_INPUT) {
        printf ("No input\n");
        return 1;
    }

    if (rc == TOO_LONG) {
        printf ("Input too long\n");
        return 1;
    }

    printf ("OK [%s]\n", buff);

    return 0;
}

它提供了与fgets防止缓冲区溢出相同的保护,但它也将发生的情况通知调用者并清除了多余的字符,以使它们不会影响您的下一个输入操作。

随意使用它,我特此根据“做你该死的想要的”许可发布它:-)


实际上,原始的C99标准并未gets()在定义的第7.19.7.7节或第7.26.9节的未来库说明以及的子节中明确弃用<stdio.h>。甚至没有脚注说明它很危险。(说到这里,我看到“它弃用ISO / IEC 9899:1999 / Cor.3:2007(E))”中的答案俞灏),但C11也从标准中删除它-而不是之前的时间!
Jonathan Leffler

int getLine (char *prmpt, char *buff, size_t sz) { ... if (fgets (buff, sz, stdin) == NULL)隐藏size_tint的转换szsz > INT_MAX || sz < 2将捕获的奇怪值sz
恢复莫妮卡

if (buff[strlen(buff)-1] != '\n') {是一种黑客利用方式,因为邪恶用户输入的第一个字符可能是嵌入的空字符呈现buff[strlen(buff)-1]UB。 while (((ch = getchar())...用户输入空字符时会遇到麻烦。
chux-恢复莫妮卡


6

您必须先破坏API才能删除API函数。如果愿意,许多应用程序将不再编译或运行。

这就是一个参考文献给出的原因:

读取使s指向的数组溢出的行将导致未定义的行为。建议使用fgets()。


4

最近我看到,在USENET帖子comp.lang.c,该gets()正从标准中删除。OH

您会很高兴知道委员会刚刚投票(结果一致同意)也从草案中删除了gets()。


3
将其从标准中删除是非常好的。但是,由于向后兼容,大多数实现将至少在未来20年内将其作为“现在的非标准扩展”提供。
乔纳森·勒夫勒

1
是的,没错,但是当您使用gcc -std=c2012 -pedantic ...gets()进行编译时将无法通过。(我刚刚编造了-std参数)
pmg

4

在C11(ISO / IEC 9899:201x)中,gets()已被删除。(在ISO / IEC 9899:1999 / Cor.3:2007(E)中已弃用)

除此以外fgets(),C11还引入了一种新的安全替代方法gets_s()

C11 K.3.5.4.1 gets_s功能

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

但是,在“ 推荐做法”部分,fgets()仍然是首选。

fgets函数允许编写正确的程序安全地处理过长的输入行,以至于无法存储在结果数组中。通常,这要求调用者fgets注意结果数组中是否存在换行符。考虑使用fgets(以及基于换行符的所有必要处理)代替 gets_s


3

gets()这是很危险的,因为用户可能会在提示中输入过多内容,从而使程序崩溃。它无法检测到可用内存的结束,因此,如果为此目的分配的内存太小,则可能导致段错误和崩溃。有时,用户似乎不太可能在提示一个人名字的提示符下键入1000个字母,但是作为程序员,我们需要使程序防弹。(如果用户通过发送太多数据而使系统程序崩溃,也可能会带来安全隐患)。

fgets() 允许您指定从标准输入缓冲区中取出多少个字符,因此它们不会超出变量。


请注意,真正的危险不是使程序崩溃,而是使程序运行任意代码。(通常,利用未定义的行为。)
Tanz87

2

我谨向所有仍在gets其库中的C库维护者致以诚挚的邀请:“以防万一有人仍然依赖它”:请用

char *gets(char *str)
{
    strcpy(str, "Never use gets!");
    return str;
}

这将有助于确保没有人仍然依赖它。谢谢。


2

C gets函数是危险的,并且是非常昂贵的错误。Tony Hoare在他的演讲“空引用:十亿美元的错误”中特别提到了这一点:

http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare

整个小时都值得一看,但从30分钟起,他的评论意见就遭到了批评,其中大约39分钟就引起了批评。

希望这能引起您对整个演讲的兴趣,从而引起我们的注意,即引起我们注意如何在语言中需要更多正式的正确性证明,以及应该为语言设计人员而不是程序员指责语言设计人员的错误。这似乎是不良语言设计师以“程序员自由”为幌子将责怪推给程序员的全部可疑原因。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.