October 9th, 2009

LoadString can load strings with embedded nulls, but your wrapper function might not

Whenever somebody reports that the SHFileOperation function or the lpstrFilter member of the OPENFILENAME structure is not working, my psychic powers tell me that they failed to manage the double-null-terminated strings.

Since string resources take the form of a counted string, they can contain embedded null characters, since the null character is not being used as the string terminator. The LoadString function knows about this, but other functions might not.

Here’s one example:

TCHAR szFilters[80];
strcpy_s(szFilters, 80, "Text files\0*.txt\0All files\0*.*\0");
// ... or ...
strlcpy(szFilters, "Text files\0*.txt\0All files\0*.*\0", 80);

The problem is that you’re using a function which operates on null-terminated strings but you’re giving it a double-null-terminated string. Of course, it will stop copying at the first null terminator, and the result is that szFilters is not a valid double-null-terminated string.

Here’s another example:

sprintf_s(szFilters, 80, "%s\0*.txt\0%s\0*.*\0", "Text files", "All files");

Same thing here. Functions from the sprintf family take a null-terminated string as the format string. If you “embed” a null character into the format string, the sprintf function will treat it as the end of the format string and stop processing.

Here’s a more subtle example:

CString strFilter;
strFilter.LoadString(g_hinst, IDS_FILE_FILTER);

There is no obvious double-null-termination bug here, but there is if you look deeper.

BOOL CString::LoadString(UINT nID)
{
// try fixed buffer first (to avoid wasting space in the heap)
TCHAR szTemp[256];
int nCount = sizeof(szTemp) / sizeof(szTemp[0]);
int nLen = _LoadString(nID, szTemp, nCount);
if (nCount - nLen > CHAR_FUDGE)
{
*this = szTemp;
return nLen > 0;
}

// try buffer size of 512, then larger size until entire string is retrieved
int nSize = 256;
do
{
nSize += 256;
nLen = _LoadString(nID, GetBuffer(nSize - 1), nSize);
} while (nSize - nLen <= CHAR_FUDGE);
ReleaseBuffer();

return nLen > 0;
}

Observe that this function loads the string into a temporary buffer, and then if it succeeds, stores the result via the operator= operator, which assumes a null-terminated string. If your string resource contains embedded nulls, the operator= operator will stop at the first null.

The mistake here was taking a class designed for null-terminated strings and using it for something that isn’t a null-terminated string. After all, it’s called a CString and not a CDoubleNullTerminatedString.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

0 comments

Discussion are closed.