【问题标题】:How do you call vsnprintf() safely?你如何安全地调用 vsnprintf() ?
【发布时间】:2016-06-13 11:12:49
【问题描述】:

我正在将一些非常古老(> 10 年)的 C 代码移植到现代 Linux 上。我在自定义编写的 vsnprintf() 包装器中遇到分段错误(显然它的任务是检测重复的输出字符串并实习它们):

char* strVPrintf(const String fmt, va_list ap)
{
  /* Guess we need no more than 50 bytes. */
  int n, size = 50;
  char* p = (char*)memMalloc(size), q;

  while (1) {
    /* Try to print in the allocated space. */
    n = vsnprintf(p, size, fmt, ap);
    /* If that worked, return the string. */
    if (n > -1 && n < size) {
      break;
    }
    /* Else try again with more space. */
    if (n > -1)                /* glibc 2.1 */
      size = n + 1;            /* precisely what is needed */
    else                   /* glibc 2.0 */
      size *= 2;               /* twice the old size */
    p = memRealloc(p, size);
  }

  q =  strRegister(p);
  memFree(p);
  return q;
}

作者似乎假设标准vsnprintf() 函数返回写入的字符数,如果它没有接收到足够的空间来格式化所有args,则简单地返回一个标记值。这意味着您可以猜测缓冲区大小并在必要时增加它。

但是在我的系统(Ubuntu 14.04,glibc 2.19)上,当使用太多参数调用提供的空间时,vnprintf 会导致分段错误。与此同时,snprintf() 家族的语义是否发生了巨大变化?确保您提供足够缓冲空间的现代方法是什么?

【问题讨论】:

  • man vsnprintf: 函数 snprintf() 和 vsnprintf() 不会写入超过 size 字节(包括终止空字节 ('\0'))。如果输出由于此限制而被截断,则返回值是字符数(不包括终止的空字节),如果有足够的空间可用,这些字符将被写入最终字符串。因此,大小或更大的返回值意味着输出被截断。 程序的那部分似乎是正确的。
  • 你没有检查内存分配失败。
  • 什么是String?我怀疑是char *,但您也应该提供该信息。
  • @AndrewHenle 你是对的,解决了这个问题。
  • 附带问题:1) 我希望size_t size 而不是int。 2) IMO n &gt;= 0n &gt; -1 更容易关注。

标签: c printf glibc


【解决方案1】:

这是在除 SunOS 4(已过时 20 年)以外的所有操作系统上使用 snprintfvsnprintf 的正确方法,因此您的问题出在其他地方。

我会做一个纯粹的猜测并说我几乎可以肯定你的问题是你将 va_list ap 传递给 vsnprintf 消耗它然后你希望它在下一个被重置称呼。这是不正确的,并且多年前已停止在 gcc 中工作(因为它仅适用于某些架构)。

变化:

n = vsnprintf(p, size, fmt, ap);

收件人:

va_list apc;
va_copy(apc, ap);
n = vsnprintf(p, size, fmt, apc);
va_end(apc);

看看是否有帮助。

这是一个简单的测试,看看发生了什么:

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

void
foo(const char *fmt, va_list ap)
{
#ifdef BAD
    vprintf(fmt, ap);
#else
    va_list apc;
    va_copy(apc, ap);
    vprintf(fmt, apc);
    va_end(apc);
#endif
    vprintf(fmt, ap);
}

void
bar(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    foo(fmt, ap);
    va_end(ap);
}

int
main(int argc, char **argv)
{
    bar("foo %s\n", "bar");
    return 0;
}

运行时我得到这个:

$ cc -o foo foo.c && ./foo
foo bar
foo bar
$ cc -DBAD -o foo foo.c && ./foo
foo bar
foo ����

【讨论】:

  • 你说得对,这就是问题所在。像往常一样,我被短字符串打印没有问题但长字符串崩溃的事实分心,所以我认为缓冲区溢出有问题。我应该知道,未定义的行为是永远不可信任的,如果你给它机会,伪装成另一种问题!
【解决方案2】:

据我了解代码,其目的是检测sprintf 将输出字符串完全写入缓冲区所需的大小。有一个函数可以为您执行此操作:asprintf(或此处为 vasprintf)。

原型:

int vasprintf(char **strp, const char *fmt, va_list ap);

按如下方式使用:

String strVPrintf(const String fmt, va_list ap)
{
    char *ans;
    int n;
    n = vasprintf(&ans, fmt, ap);
    // do the checks
    return ans;
}

有了这个函数,我想你就不再需要这个包装器了。

【讨论】:

  • asprintf()vasprintf() 是非标准函数。
  • @AndrewHenle OP 使用 glibc。
  • OP 没有表达过编写不可移植代码的愿望。
  • @AndrewHenle OP:我正在将一些非常古老(> 10 年)的 C 代码移植到现代 Linux 中。 (即 GNU/Linux 发行版)。 vasprintf 是 GNU libc 实现的一部分。所以这段代码可以在“Linuxes”上运行。这个问题用 glibc 标记。
  • 那是如何导致推荐编写不可移植代码的?
【解决方案3】:

不确定你的,但我的变量参数列表手册页说:

兼容性
这些宏与它们替换的历史宏不兼容。一种 向后兼容的版本可以在包含文件 中找到。

正如您所说,这是非常古老的代码,可能在此例程中收到的 va_list 不是vsnprintf 期望的 va_list。您应该首先尝试使用一个标头提取所有参数,然后再提取另一个以确保(通常vsnprintf 与 stdarg.h 兼容)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多