看看下面的代码有什么问题?

int main( void )
{
	size_t MAX = 256;	
	wchar_t * src = new wchar_t[MAX];
	src[0] = L'H';
	src[1] = L'i';
	char *dest = new char[MAX];
	size_t len = 0;

	wcstombs_s(&len,dest,MAX,src,MAX);
}

如果我们使用strlen(dest),得到的结果会是0,但是如果我们访问dst[1],其结果是i,是我们期望的,而且如果src的有效字符更多一些,除了第一个字符不在外,其他的都是有的,为什么第一个字符消失了呢?

这是我碰到的一个使用New CRT的一个bug,觉得New CRT还挺有意思的,所以找来研究一下:

New CRT最显著的变化叫做Paramter Validation和Sized  Buffers。New CRT中_s的若干API并不是要替代旧的API,也就是说,strcpy还是strcpy,New CRT会引入一个strcpy_s。但是如果编译时使用了新的library,旧的方法就会被标注为 过时的,也就是会得到编译器的警告信息。下面借用Michael的例子(strncat,strncat_s)来说说NEW CRT的常见错误用法。

char *strncat(
   char *strDest,
   const char *strSource,
   size_t count 
);

errno_t strcat_s(
   char *strDestination,
   size_t sizeInBytes,
   const char *strSource 
);

strncat最后一个参数是Number of characters to append,也就是strDest中剩余的空间或者是要append的数量,而不是strDest的 buffer size。这很容易造成Off-by-one或者Off-by-many的错误。看下面的例子:

int main( void )
{
    size_t MAX = 256;    
    wchar_t * src = new wchar_t[MAX];
    src[0] = L'H';
    src[1] = L'i';
    char *dest = new char[MAX];
    size_t len = 0;

    wcstombs_s(&len,dest,MAX,src,MAX);

    char szTarget[12];
    char *s = "Hello, World";
    
    strncpy(szTarget,s,sizeof(szTarget));
    strncat(szTarget,s,sizeof(szTarget));

}

看看在strncat执行之前的内存:

before
再看看在strncat执行之后的内存:

after

strncat发现从0040FAF0处为0(字符串结束的位置),故从此处开始做append。最后得到的结果如图。但是,对比之前的内存,可以发现原来len的空间被覆盖了,也就是发生了stack corrupt。

一种解决的办法如下所示:

char szTarget[12];
char *s = "Hello, World";

strncpy(szTarget,s,sizeof(szTarget));
strncat(szTarget,s,strlen(szTarget) – strlen(s));

这种方法的问题在于, If the length of the destination buffer is exactly the same length as the source buffer, many n-functions do not null-terminate the destination buffer, so the call to strlen(szTarget) could return a length greater than the length of the target because there is no trailing '\0' character. 上面的代码中strlen(szTarget)返回20。
更好的方式是用如下的代码:

char szTarget[12];
char *s = "Hello, World";

size_t cSource = strnlen_s(s,20);
strncpy_s(szTarget,sizeof(szTarget),s,cSource);
strncat_s(szTarget,sizeof(szTarget),s,cSource);

如果发现空间不够,就会Assert,而不会造成memory corruption。
下面是一段来源于某公司的代码,本意是想写出安全的代码。但是它的问题在于,如果输入的字符串是NULL,strncpy就会fail了。

void noOverflow(char *str)
{
  char buffer[10];
  strncpy(buffer,str,(sizeof(buffer)-1));
  buffer[(sizeof(buffer)-1)]=0;
  /* Avoiding buffer overflow with the above two lines */
}

strncat_s不会fail的原因是更新的runtime 执行更加严格的parameter check,下面就是strcat_s的参数检查部分的代码:

/* validation section */
_VALIDATE_RETURN_ERRCODE(front != NULL, EINVAL);
_VALIDATE_RETURN_ERRCODE(sizeInTChars > 0, EINVAL);
_VALIDATE_RETURN_ERRCODE(back != NULL || count == 0, EINVAL);

 

需要注意的另外一点是*_s函数和strsafe函数的区别:*_s函数在检测到一个error时,会将结果字符串设置为NULL,而strsafe函数比如StringCchCopyStringCchCat在碰到任何错误的时候,其默认行为是尽量填充结果字符串,并最后使用NULL结束之。可以使用下面函数来模仿*_s函数:

StringCchCatEx(dst,sizeof(dst)/sizeof(dst[0]),src,NULL,NULL,STRSAFE_NULL_ON_FAILURE)

 

 

 

 

Reference: Saying Goodbye to an Old Friend, by Michael Howard, http://blog.yezhucn.com/dncode/secure03102004.htm