可变修改类型兼容性及其安全隐患
Posted
技术标签:
【中文标题】可变修改类型兼容性及其安全隐患【英文标题】:Variably-modified types compatibility and its security implications 【发布时间】:2015-04-19 21:31:24 【问题描述】:我对 C99 的可变修改类型系统产生了浓厚的兴趣。这个问题的灵感来自this one。
检查这个问题的代码,我发现了一些有趣的东西。考虑这段代码:
int myFunc(int, int, int, int[][100]);
int myFunc(int a, int b, int c, int d[][200])
/* Some code here... */
这显然不会(也不会)编译。但是,这段代码:
int myFunc(int, int, int, int[][100]);
int myFunc(int a, int b, int c, int d[][c])
/* Some code here... */
在没有警告的情况下编译(在 gcc 上)。
这似乎意味着可变修改的数组类型与任何非可变修改的数组类型兼容!
但这还不是全部。您会期望变量修改的类型至少会打扰使用哪个变量来设置其大小。但它似乎没有这样做!
int myFunc(int, int b, int, int[][b]);
int myFunc(int a, int b, int c, int d[][c])
return 0;
也编译没有任何错误。
所以,我的问题是:这是正确的标准化行为吗?
另外,如果可变修改的数组类型真的可以与任何具有相同维度的数组兼容,这是否意味着令人讨厌的安全问题?例如,考虑以下代码:
int myFunc(int a, int b, int c, int d[][c])
printf("%d\n", sizeof(*d) / sizeof((*d)[0]));
return 0;
int main()
int arr[10] = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9;
myFunc(0, 0, 100, &arr);
return 0;
编译并输出 100,没有错误或警告,什么都没有。在我看来,这意味着即使您通过sizeof
严格检查数组的大小,也可以轻松地进行越界数组写入,不进行一次强制转换,甚至打开所有警告!还是我错过了什么?
【问题讨论】:
如果您还没有,请尝试将 -std=c99 -pedantic-errors 添加到您的 gcc 编译行,看看是否有任何不同。 @jschultz410: 好主意,但不——它根本没有区别 =( 在很多情况下编译器不可能静态推断出 c 的值(例如 - c 是从标准输入输入的)。因此,通常不可能对此类函数定义的参数进行任何有意义的静态类型检查。如果你这样做,编译器会说“好的,我允许你传递任何你想要的 d,只要它的类型是一个双索引整数数组。祝你好运!” 在这样的函数中,对于具有不同 c 值的不同调用会发生什么?它是否通过基于 c 动态计算它应该在内存中前进多远来做正确的事情? @jschultz410:我不确定我理解你的意思......你能举个例子吗? 【参考方案1】:C99,第 6.7.5.2 节似乎是给出相关规则的地方。特别是,
第 6 行:
对于要兼容的两个数组类型,两者都应具有兼容的元素类型,并且如果两个大小说明符都存在并且都是整数常量表达式,则两个大小说明符应具有相同的常量值。如果在需要它们兼容的上下文中使用这两种数组类型,则如果这两个大小说明符的计算结果不相等,则为未定义行为。
以前的,现在已删除的答案也引用了第 6 行。对该答案的评论认为,第二句受第一句末尾的条件限制,但这似乎不太可能阅读。该部分的示例 3 可以澄清(摘录):
int c[n][n][6][m];
int (*r)[n][n][n+1];
r=c; // compatible, but defined behavior only if
// n == 6 and m == n+1
这似乎与问题中的示例相当:两种数组类型,一种具有恒定维度,另一种具有相应的可变维度,并且需要兼容。当运行时变量维度与编译时常量维度不同时,行为未定义(示例 3 中的每个注释和 6.7.5.2/6 的一个合理解读)。无论如何,未定义的行为难道不是您所期望的吗?不然为什么要提这个问题?
假设我们可以同意当这种不匹配发生时行为是未定义的,我观察到编译器通常不需要识别未定义或可能未定义的行为,也不需要发布任何类型的诊断,如果他们确实识别了这种情况。在这种情况下,我希望编译器能够警告可能未定义的行为,但它必须成功编译代码,因为它在语法上是正确的并且满足所有适用的约束。请注意,能够警告此类使用的编译器可能不会在默认情况下这样做。
【讨论】:
谢谢,这是一个很好的答案!我认为你是对的,我对这条规则的阅读可能是不正确的。我可能太习惯于键入不兼容是 UB 原因第一...这使 C 标准清晰,但不是 gcc =) 无论如何,咆哮它不发出警告可能不会有多大好处... 如果由我决定,我会明确声明 VM 类型与所有内容不兼容,以确保 =) 有道理,IMO。 但是@Mints,VM 类型必须至少与它们自己兼容,否则它们将无法使用。对于具有完全相同声明符的 VM 类型,您仍然可以有未定义的行为,但是,在运行时变量维度不同时。鉴于这个问题,通过使 VM 类型自动与其他所有内容不兼容可以获得什么?这是 C 为程序员提供强大工具的另一个例子,他可以利用这些工具造成强大的破坏。 C 不适合青少年。 (没有暗示任何东西。)【参考方案2】:#include <stdio.h>
void foo(int c, char d[][c])
fprintf(stdout, "c = %d; d = %p; d + 1 = %p\n", c, d, d + 1);
int main()
char x[2][4];
char y[3][16];
char (*z)[4] = y; /* Warning: incompatible types */
foo(4, x);
foo(16, y);
foo(16, x); /* We are lying about x. What can / should the compiler / code do? */
foo(4, y); /* We are lying about y. What can / should the compiler / code do? */
return 0;
输出:
c = 4; d = 0x7fff5b295b70; d + 1 = 0x7fff5b295b74
c = 16; d = 0x7fff5b295b40; d + 1 = 0x7fff5b295b50
c = 16; d = 0x7fff5b295b70; d + 1 = 0x7fff5b295b80
c = 4; d = 0x7fff5b295b40; d + 1 = 0x7fff5b295b44
所以,foo() 确实会根据 c 动态计算出 d 前进多远,正如您的代码所展示的那样。
但是,编译器通常不可能静态确定您是否/何时错误地调用 foo()。似乎如果你这样做,那么编译器会说“好的,我允许你传递你想要的任何东西作为 d,只要它的类型是一个双索引的字符数组。指针 d 的操作将被确定c. 祝你好运!”
也就是说,是的,编译器通常不能对这些类型的参数进行静态类型检查,因此几乎可以肯定该标准并不强制编译器捕获所有可能静态确定类型不兼容的情况。
【讨论】:
是的,因为sizeof(*d)
是使用c
计算的,正如我的问题中所示 =)
“但是,编译器通常无法确定您是否/何时错误地调用 foo()”。这正是问题所在。如果这完全杀死了静态类型检查,为什么我什至可以打电话给foo
?此外,可以使用动态类型检查。对于动态sizeof
调用,VLA 实现无论如何都必须将其大小保持为某种形式的元数据,所以如果这个大小与传递的参数类型的大小不匹配,为什么不使用 segfault 或转到 UB作为?
@Mints97:C 不需要动态类型检查。如果大小不匹配,则行为未定义;它确实,正如你所说,“去 UB”。未定义的行为并不意味着您的程序会崩溃。这意味着行为未定义。这通常包括看起来“工作”的程序。以上是关于可变修改类型兼容性及其安全隐患的主要内容,如果未能解决你的问题,请参考以下文章