当 vsnprintf 不可用时安全地格式化字符串

Format strings safely when vsnprintf is not available

我正在编写需要格式化字符串的代码,我想避免缓冲区溢出。

我知道如果 vsnprintf 可用(从 C99 开始)我们可以这样做:

char* formatString(const char *format, ...)
{
    char* result = NULL;
    va_list ap;
    va_start(ap, format);

    /* Get the size of the formatted string by getting vsnprintf return the
     * number of remaining characters if we ask it to write 0 characters */
    int size = vsnprintf(NULL, 0, format, ap);

    if (size > 0)
    {
        /* String formatted just fine */
        result = (char *) calloc(size + 1, sizeof(char));
        vsnprintf(result, size + 1, format, ap);
    }

    va_end(ap);
    return result;
}

我想不出在 C90 中做类似事情的方法(没有 vsnprintf)。如果事实证明不编写极其复​​杂的逻辑是不可能的,我很乐意为结果设置最大长度,但我不确定如何在不冒缓冲区溢出风险的情况下实现这一点。

C99 之前的版本没有提供简单的解决方案来格式化具有高度安全性的字符串,以防止缓冲区溢出 运行s。

正是那些讨厌的 "%s""%[]""%f" 格式说明符需要非常仔细地考虑它们潜在的长输出。因而需要这样的功能。

对于那些早期的编译器来说,这样做需要代码来分析 format 和参数来找到所需的大小。到那时,代码几乎可以让您拥有完整的 my_vsnprintf()。我会为此寻求现有的解决方案。 .


即使使用 C99,*printf() 也有环境限制。

The number of characters that can be produced by any single conversion shall be at least 4095. C11dr §7.21.6.1 15

因此,任何试图 char buf[10000]; snprintf(buf, sizeof buf, "%s", long_string); 的代码都可能会出现问题,即使 buf[] 足够 strlen(long_string) > 4095

这意味着快速而肮脏的代码可以计算 % 和格式长度,并合理假设所需的大小不超过:

size_t sz = 4095*percent_count + strlen(format) + 1;

当然,对说明符的进一步分析可能会导致更保守 sz。继续 path we end 编写我们自己的 my_vsnprintf().


即使使用您自己的 my_vsnprintf()安全性 也仅此而已。没有 运行 时间检查 format(可能是动态的)是否与以下参数匹配。为此需要一种新方法。

C99 解决方案厚颜无耻的自我宣传,以确保匹配说明符和参数:Formatted print without the need to specify type matching specifiers using _Generic

正在转移回答。

The main reason vsnprintf() was added to C99 was that it is hard to protect vsprintf() or similar. One workaround is to open /dev/null, use vfprintf() to format the data to it, note how big a result was needed, and then decide whether it is safe to proceed. Icky, especially if you open the device on each call.

这意味着您的代码可能会变成:

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

extern char *formatString(const char *format, ...);

char *formatString(const char *format, ...)
{
    static FILE *fp_null = NULL;

    if (fp_null == NULL)
    {
        fp_null = fopen("/dev/null", "w");
        if (fp_null == NULL)
            return NULL;
    }

    va_list ap;

    va_start(ap, format);
    int size = vfprintf(fp_null, format, ap);
    va_end(ap);

    if (size < 0)
        return NULL;

    char *result = (char *) malloc(size + 1);
    if (result == NULL)
        return NULL;

    va_start(ap, format);
    int check = vsprintf(result, format, ap);
    va_end(ap);

    assert(check == size);

    return result;
}

int main(void)
{
    char *r1 = formatString("%d Dancing Pigs = %4.2f%% of annual GDP (grandiose dancing pigs!)\n",
                            34241562, 21.2963);
    char *r2 = formatString("%s [%-13.10s] %s is %d%% %s\n", "Peripheral",
                            "sub-atomic hyperdrive", "status", 99, "of normality");

    if (r1 != NULL)
        printf("r1 = %s", r1);

    if (r2 != NULL)
        printf("r2 = %s", r2);

    free(r1);
    free(r2);
    return 0;
}

由于在函数内部用fp_null静态变量写入,文件流无法关闭。如果这很麻烦,请将其作为文件中的变量并为 if (fp_null != NULL) { fclose(fp_null); fp_null = NULL; }.

提供一个函数

我毫无歉意地假设一个类 Unix 环境 /dev/null;如果你在 Windows.

上工作,你可以将其翻译成 NUL:

注意问题中的原始代码没有使用两次va_start()va_end()(与此代码不同);那会导致灾难。在我看来,尽可能将 va_end() 放在 va_start() 之后是个好主意 — 如这段代码所示。显然,如果您的函数本身正在逐步通过 va_list,那么会有比此处显示的更大的差距,但是当您只是像此处那样将变量参数中继到另一个函数时,应该只有一行介于两者之间。

使用 GCC 8.2.0(在 macOS 10.13 High Sierra 上编译)和命令行在 Mac 运行ning macOS 10.14 Mojave 上编译代码:

$ gcc -O3 -g -std=c90 -Wall -Wextra -Werror -Wmissing-prototypes \
>     -Wstrict-prototypes vsnp37.c -o vsnp37
$

当运行时,它产生:

r1 = 34241562 Dancing Pigs = 21.30% of annual GDP (grandiose dancing pigs!)
r2 = Peripheral [sub-atomic   ] status is 99% of normality