vsnprintf不可用时,请安全格式化字符串

时间:2018-09-27 12:35:36

标签: c printf c89

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

我知道,如果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)。如果事实证明,如果不编写极其复​​杂的逻辑是不可能的,那么我很乐意为结果设置最大长度,但是我不确定在不冒缓冲区溢出风险的情况下如何实现这一目标。

2 个答案:

答案 0 :(得分:2)

Pre-C99无法简单地解决格式化字符串的问题,并且具有防止缓冲区溢出的高度安全性。

正是那些令人讨厌的"%s""%[]""%f"格式说明符需要对潜在的长输出量进行仔细的考虑。因此需要这种功能。 @Jonathan Leffler

为此,那些早期的编译器必须执行代码来分析format并使用参数来查找所需的大小。到那时,几乎可以使用代码来拥有完整的my_vsnprintf()。我会为此寻求现有的解决方案。 @user694733


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

  

任何一次转换可以产生的字符数至少应为4095。C11dr§7.21.6.115

因此,即使有足够的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,我们end继续编写自己的my_vsnprintf()


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

C99解决方案的厚脸皮自我广告,以确保匹配的说明符和参数:Formatted print without the need to specify type matching specifiers using _Generic

答案 1 :(得分:1)

转移comments来回答。

  

vsnprintf()添加到C99的主要原因是,很难保护vsprintf()或类似的东西。一种解决方法是打开/dev/null,使用vfprintf()格式化数据,注意需要多大的结果,然后确定是否安全进行。麻烦,尤其是在每次通话中打开设备时。

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

#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; }提供一个函数。

我毫不犹豫地假设一个带有/dev/null的类Unix环境;如果您使用的是Windows,则可以将其翻译为NUL:

请注意,问题中的原始代码没有两次使用va_start()va_end()(不同于此代码);那会导致灾难。我认为,最好将va_end()放在va_start()之后,如代码所示。显然,如果您的函数本身正在逐步通过va_list,则差距将比此处显示的要大,但是,当您只是将变量参数中继到另一个函数(如此处)时,应该只有一行在两者之间。

代码使用GCC 8.2.0(在macOS 10.13 High Sierra上编译)通过命令行在运行macOS 10.14 Mojave的Mac上干净地编译:

$ 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