我想出了这个小代码,但所有专业人士都说它很危险,我不应该写这样的代码。任何人都可以在“更多”细节中突出其漏洞吗?
int strlen(char *s){
return (*s) ? 1 + strlen(s + 1) : 0;
}
它本身没有漏洞,这是完全正确的代码。当然,它过早地悲观了。除了最短的字符串,它的堆栈空间会用完,并且由于递归调用,它的性能会很糟糕,但除此之外没关系。
尾调用优化很可能无法处理此类代码。如果你想危险地生活并依赖尾调用优化,你应该改写它以使用尾调用:
// note: size_t is an unsigned integertype
int strlen_impl(const char *s, size_t len) {
if (*s == 0) return len;
if (len + 1 < len) return len; // protect from overflows
return strlen_impl(s+1, len+1);
}
int strlen(const char *s) {
return strlen_impl(s, 0);
}
有点危险,但它是不必要的递归,并且可能比迭代替代方案效率低。
我还认为给定一个很长的字符串存在堆栈溢出的危险。
这段代码中有两个严重的安全漏洞:
使用 ofint
而不是size_t
返回类型。如所写,字符串长于INT_MAX
将导致此函数通过整数溢出调用未定义的行为。在实践中,这可能会导致计算strlen(huge_string)
为像 1 这样malloc
的小值,错误的内存量,然后执行strcpy
到它,导致缓冲区溢出。
可以溢出堆栈的无限递归,即堆栈溢出。:-) 编译器可能会选择将递归优化为循环(在这种情况下,使用当前的编译器技术是可能的),但不能保证它会。在最好的情况下,堆栈溢出只会使程序崩溃。在最坏的情况下(例如在没有保护页面的线程上运行)它可能会破坏不相关的内存,可能导致任意代码执行。
已经指出的杀死堆栈的问题应该由一个体面的编译器修复,其中明显的递归调用被展平为一个循环。我验证了这个假设并要求 clang 翻译你的代码:
//sl.c
unsigned sl(char const* s) {
return (*s) ? (1+sl(s+1)) : 0;
}
编译和反汇编:
clang -emit-llvm -O1 -c sl.c -o sl.o
# ^^ Yes, O1 is already sufficient.
llvm-dis-3.2 sl.o
这是llvm结果的相关部分(sl.o.ll)
define i32 @sl(i8* nocapture %s) nounwind uwtable readonly {
%1 = load i8* %s, align 1, !tbaa !0
%2 = icmp eq i8 %1, 0
br i1 %2, label %tailrecurse._crit_edge, label %tailrecurse
tailrecurse: ; preds = %tailrecurse, %0
%s.tr3 = phi i8* [ %3, %tailrecurse ], [ %s, %0 ]
%accumulator.tr2 = phi i32 [ %4, %tailrecurse ], [ 0, %0 ]
%3 = getelementptr inbounds i8* %s.tr3, i64 1
%4 = add i32 %accumulator.tr2, 1
%5 = load i8* %3, align 1, !tbaa !0
%6 = icmp eq i8 %5, 0
br i1 %6, label %tailrecurse._crit_edge, label %tailrecurse
tailrecurse._crit_edge: ; preds = %tailrecurse, %0
%accumulator.tr.lcssa = phi i32 [ 0, %0 ], [ %4, %tailrecurse ]
ret i32 %accumulator.tr.lcssa
}
我没有看到递归调用。事实上,clang 调用了循环标签tailrecurse
,它为我们提供了关于 clang 在这里做什么的指针。
所以,最后(tl;dr)是的,这段代码是完全安全的,一个体面的编译器和一个体面的标志将消除递归。