如何在C语言中防止scanf引起缓冲区溢出?

99

我使用这段代码:

while ( scanf("%s", buf) == 1 ){

如何最好地防止缓冲区溢出,以便可以传递随机长度的字符串?

我知道我可以通过调用例如:

while ( scanf("%20s", buf) == 1 ){

但我希望能够处理用户输入的任何内容。 或者使用scanf不能安全地完成这项任务,我应该使用fgets吗?

7个回答

82

在他们的书《编程实践》中(非常值得一读),Kernighan和Pike讨论了这个问题,并通过使用snprintf()创建具有正确缓冲区大小的字符串来解决该问题,从而将其传递给scanf()函数系列。 实际上:

int scanner(const char *data, char *buffer, size_t buflen)
{
    char format[32];
    if (buflen == 0)
        return 0;
    snprintf(format, sizeof(format), "%%%ds", (int)(buflen-1));
    return sscanf(data, format, buffer);
}

注意,这仍然将输入限制为提供的"buffer"大小。如果需要更多空间,则必须进行内存分配或使用非标准库函数进行内存分配。


请注意,POSIX 2008(2013)版本的scanf()函数族支持格式修饰符m(赋值-分配字符)用于字符串输入(%s%c%[)。m不像以前接受char *参数,而是接受char **参数,并为它读取的值分配必要的空间:

char *buffer = 0;
if (sscanf(data, "%ms", &buffer) == 1)
{
    printf("String is: <<%s>>\n", buffer);
    free(buffer);
}
如果sscanf()函数未满足所有转换规范,那么在该函数返回之前,为%ms类型转换所分配的所有内存都将被释放。

@Sam:是的,应该是buflen-1 — 谢谢。然后你需要担心无符号下溢(回绕到一个相当大的数字),因此需要进行if测试。我会非常想用assert()替换它,或者在开发期间在if之前备份它,如果有人粗心地传递0作为大小,则会触发。我没有仔细审查%0ssscanf()的含义 —— 测试可能更好,如if (buflen < 2) - Jonathan Leffler
所以 snprintf 将一些数据写入字符串缓冲区,而 sscanf 从创建的字符串中读取。它在哪里替换了 scanf 以从 stdin 中读取? - krb686
你使用“format”一词作为结果字符串并将其作为第一个参数传递给snprintf,但它实际上不是格式参数,这也相当令人困惑。 - krb686
@krb686:这段代码是为了让要扫描的数据在参数“data”中,因此使用“sscanf()”是合适的。如果你想从标准输入读取,只需删除“data”参数并调用“scanf()”。至于将变量命名为“format”,以成为调用“sscanf()”中格式字符串的变量,你可以根据需要更改它的名称,但它的名称并不不准确。我不确定有什么其他替代方案是有意义的;将其命名为“in_format”是否会更清晰?我不打算在这个代码中进行更改;如果你在自己的代码中使用这个想法,可以更改它。 - Jonathan Leffler
1
@mabraham:在macOS Sierra 10.12.5下仍然是正确的(截至2017-06-06) - 在macOS上,scanf()不支持文档化的%ms,尽管它很有用。 - Jonathan Leffler
显示剩余2条评论

35

如果你正在使用gcc,你可以使用GNU扩展的 a 说明符,让scanf()为你分配内存来保存输入:

int main()
{
  char *str = NULL;

  scanf ("%as", &str);
  if (str) {
      printf("\"%s\"\n", str);
      free(str);
  }
  return 0;
}

编辑: 正如Jonathan所指出的,你应该查看scanf手册页面,因为说明符可能不同(%m),并且在编译时可能需要启用某些定义。

Edit: 正如Jonathan所指出的,您应该查阅scanf手册页,因为说明符可能不同(%m),并且在编译时可能需要启用某些定义。


8
这更多是使用 GNU C 库(glibc)的问题,而不是使用 GNU C 编译器的问题。 - Jonathan Leffler
4
请注意,POSIX 2008标准提供了m修饰符来完成同样的工作。请参见scanf()。你需要检查你使用的系统是否支持这个修饰符。 - Jonathan Leffler
5
GNU(至少在Ubuntu 13.10上找到的)支持%ms。符号%a是输出十六进制浮点数据时%f的同义词。GNU scanf()的man页面表示:如果程序是使用gcc -std=c99gcc -D_ISOC99_SOURCE编译的,则不可用(除非还指定了_GNU_SOURCE),在这种情况下,a被解释为浮点数的说明符(参见上文)。 - Jonathan Leffler

8
大多数情况下,fgetssscanf的组合可以完成任务。如果输入格式良好,则另一种方法是编写自己的解析器。同时请注意,第二个示例需要进行一些修改才能安全使用:
#define LENGTH          42
#define str(x)          # x
#define xstr(x)         str(x)

/* ... */ 
int nc = scanf("%"xstr(LENGTH)"[^\n]%*[^\n]", array); 

上述代码丢弃输入流,直到但不包括换行符(\n)字符。你需要添加一个 getchar() 以消耗它。

1
array needs to be char array[LENGTH+1]; - jxh
@RolandIllig 你是说12年吗? ;) - Antony Hatchkins

4
直接使用scanf(3)及其变体存在一些问题。通常,用户和非交互式用例是基于输入行来定义的。很少见到这样的情况:如果没有足够的对象,则更多的行就能解决问题,然而这是scanf的默认模式。(如果用户不知道在第一行输入一个数字,第二行和第三行可能也不会有帮助。)
至少,如果您使用fgets(3),您就知道程序需要多少输入行,并且您不会遇到任何缓冲区溢出的问题...

1

限制输入的长度肯定更容易。您可以使用循环接受任意长度的输入,每次读取一点,并根据需要重新分配字符串的空间...

但这是很多工作,所以大多数C程序员只是在某个任意长度处截断输入。我想你已经知道了,但使用fgets()并不会允许您接受任意数量的文本 - 您仍然需要设置限制。


那么,有人知道如何使用scanf实现这个吗? - goe
3
在循环中使用fgets可以让你接受任意数量的文本 - 只需不断地realloc()你的缓冲区。 - bdonlan

0

编写一个函数为您的字符串分配所需的内存并不需要太多工作。

这是我一段时间前编写的一个小型C函数,我总是用它来读取字符串。

它将返回读取的字符串,或者如果发生内存错误,则返回NULL。但请注意,您必须使用free()释放您的字符串,并始终检查其返回值。

#define BUFFER 32

char *readString()
{
    char *str = malloc(sizeof(char) * BUFFER), *err;
    int pos;
    for(pos = 0; str != NULL && (str[pos] = getchar()) != '\n'; pos++)
    {
        if(pos % BUFFER == BUFFER - 1)
        {
            if((err = realloc(str, sizeof(char) * (BUFFER + pos + 1))) == NULL)
                free(str);
            str = err;
        }
    }
    if(str != NULL)
        str[pos] = '\0';
    return str;
}

1
sizeof(char) 的定义是 1。你在这里不需要它。 - RastaJedi
1
通常最好的做法是在同一级别上保持指针分配/释放,这意味着您的函数不应该自己分配内存,因为调用者必须释放它。大多数标准库/posix函数都遵循此原则,通过返回静态字符串(如strerror(3))或期望传入预分配的字符串(如(strerror_r(3) - 或 scanf(3))... - Michael Beer
1
这段代码绝对是错误的。str[pos] = getchar() 无法检查函数返回的特殊值 EOF。 - Antti Haapala -- Слава Україні

0

总体而言:

  • 如果用户输入超过了数字变量可以容纳的最大大小,没有阅读函数可以一次性读取用户的全部输入!

  • 在32位处理器中,最大的数字是2^32-1,在64位处理器中,最大的数字是2^64-1。

  • 大多数阅读函数,如果不是全部,都使用整数来表示输入的大小,而在那里可以使用的最大数字是2^32-1。

关于scanf()

  • 如果在循环中使用它,它会忽略空格和换行符。

  • 可以用来检测输入是否为整数。

  • 如果用户不知道如何正确使用它,它和其他任何函数一样有害!

关于fgets()

  • 当我们谈论scanf()的大小时,没有任何区别。 如果fgets()使用不正确,也会导致缓冲区溢出。 之所以不谈论fgets()的原因是,fgets()要求您使用输入的大小,而scanf()则不需要,但这并不意味着您不应该。

  • 如果您的程序用户超过变量n所能容纳的最大大小,fgets()也会忽略其余的输入。

声明: char *fgets(char *str, int n, FILE *stream);

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

/* 2^32-1=4294967295 */

int main(){
    /* 2^32-1=4294967295 */

    char *a;
    char *b;

    if ((a=(char *)malloc(4294967295 * sizeof(char)))==NULL){
        puts("Memory could not be Allocated");
        
        return 1;
    }

    fgets(a, 4294967294, stdio);

    a[4294967294]='\0';
    
    if ((b=(char *)malloc(strlen(a) * sizeof(char)))==NULL){
        puts("Memory could not be Allocated");
        
        return 2;
    }
    
    strcpy(b, a);

    free(a);

    printf("%s\n", b);

    return 0;
}

结论:

  • 上面的代码是一个例子,说明为什么用一次函数调用读取数据是一个不好的主意!如果有人给你一个可以一次读取的输入,并且你将其保存在一个单一的数组中,然后你想要将其分成几部分,那么最开始保存在单一数组中是没有必要的,因为你可以使用scanf()逐个读取,而不需要先存储在单一数组中。你只是多做了一些不必要的计算。

你可能想要在POSIX标准的getline()函数中添加一个参考。 - undefined
没有理由。我上面写的内容也包括getLine()。我可以详细解释一下,但是没有理由。正如我所说,所有的函数都可能出问题,如果使用者不知道如何正确使用它们,而getLine()函数就是一个很好的例子。 - undefined

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