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

问题描述 投票:0回答:3

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

我知道如果

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

c printf c89
3个回答
2
投票

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

正是那些讨厌的

"%s"
"%[]"
"%f"
格式说明符需要非常仔细地考虑其潜在的长输出。 因此需要这样一个功能。 @乔纳森莱夫勒

要使用那些早期的编译器来执行此操作,代码必须分析

format
和参数以找到所需的大小。到那时,代码就差不多可以让你拥有完整的
my_vsnprintf()
了。 我会为此寻求现有的解决方案。 @user694733.


即使使用 C99,也存在环境限制

*printf()

任何单次转换可产生的字符数应至少为 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
。 继续沿着这条路径我们结束编写我们自己的
my_vsnprintf()


即使有你自己的

my_vsnprintf()
安全也只是那么好。 没有运行时检查
format
(可能是动态的)是否与以下参数匹配。 为此需要一种新方法。

C99 解决方案的厚颜无耻的自我广告,以确保匹配说明符和参数:格式化打印,无需使用 _Generic 指定类型匹配说明符。


2
投票

转移评论来回答。

C99中添加

vsnprintf()
的主要原因是难以保护
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; }

我毫无歉意地假设一个类似 Unix 的环境,并带有

/dev/null
;如果您在 Windows 上工作,可以将其翻译为
NUL:

请注意,问题中的原始代码没有使用

va_start()
va_end()
两次(与此代码不同);那会导致灾难。 在我看来,最好将
va_end()
尽可能放在
va_start()
之后 - 如这段代码所示。 显然,如果您的函数本身单步执行
va_list
,那么将会有比此处显示的更大的间隙,但是当您只是将变量参数中继到另一个函数(如此处所示)时,中间应该只有一行.

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

$ 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

0
投票

为什么不使用 vsprintf 并有一个逻辑来查看返回字节是否大于或小于缓冲区大小?

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.