bx icon indicating copy to clipboard operation
bx copied to clipboard

vsnprintf does not always nullterminate

Open JannesN opened this issue 2 years ago • 2 comments

Consider this code: char NumStr[32]; int Round = 116; bx::snprintf(NumStr, 32, "@[s:GUIText|a:000000FF]Round %i", Round);

In this case the buffer fits exactly the string WITHOUT the null terminator! In this special case the resulting string is not null terminated.

This is the writing code in vsnprintf: `StaticMemoryBlockWriter writer(_out, uint32_t(_max) );

		Error err;
		va_list argListCopy;
		va_copy(argListCopy, _argList);
		int32_t size = write(&writer, _format, argListCopy, &err);
		va_end(argListCopy);

		if (err.isOk() )
		{
			size += write(&writer, '\0', &err);
			return size - 1 /* size without '\0' terminator */;
		}
		else
		{
			_out[_max-1] = '\0';
		}`

In order for it to work correctly the writing of the null terminator has to be moved out of the if statement. Since err.isOk() will be true when the buffer was fully written!

On top of that it looks a bit to me like a return statement is missing in the else part?

JannesN avatar Oct 28 '21 11:10 JannesN

Here is a simpler example of the bug: char Buff[2]; bx::snprintf(Buff, BX_COUNTOF(Buff), "aa"); BX_ASSERT(bx::strLen(Buff) < 2, "This is bad.");

As you can see it ends up not being null terminated.

image

JannesN avatar Dec 10 '21 15:12 JannesN

Suggested fix:

	int32_t vsnprintf(char* _out, int32_t _max, const char* _format, va_list _argList)
	{
		if (1 < _max)
		{
			StaticMemoryBlockWriter writer(_out, uint32_t(_max) );

			Error err;
			va_list argListCopy;
			va_copy(argListCopy, _argList);
			int32_t size = write(&writer, _format, argListCopy, &err);
			va_end(argListCopy);
			write(&writer, '\0', &err);//Moved this out of the error check! Did not increase size.

			if (err.isOk() )
			{
				return size;//Subtraction no longer needed
			}
			else
			{
				_out[_max-1] = '\0';
			}
		}

		Error err;
		SizerWriter sizer;
		va_list argListCopy;
		va_copy(argListCopy, _argList);
		int32_t total = write(&sizer, _format, argListCopy, &err);
		va_end(argListCopy);

		return total;
	}

JannesN avatar Dec 10 '21 15:12 JannesN

Can you create failing unit test?

bkaradzic avatar May 05 '23 03:05 bkaradzic

The best to expand is the vsnprintf truncated test. It indeed fails.

TEST_CASE("vsnprintf truncated", "Truncated output buffer.")
{
	char buffer5[5]; // fit
	REQUIRE(4 == bx::snprintf(buffer5, BX_COUNTOF(buffer5), "abvg") );
	REQUIRE(0 == bx::strCmp(buffer5, "abvg") );

	char buffer7[7]; // truncate
	REQUIRE(10 == bx::snprintf(buffer7, BX_COUNTOF(buffer7), "Ten chars!") );
	REQUIRE(0  == bx::strCmp(buffer7, "Ten ch") );

	REQUIRE(6 == bx::snprintf(buffer7, BX_COUNTOF(buffer7), "Seven?!"));//Truncate exactly by one character
	REQUIRE(buffer7[BX_COUNTOF(buffer7)-1] == '\0');//Should be null terminated!
	REQUIRE(0 == bx::strCmp(buffer7, "Seven!"));//Six characters should be written
}

image

JannesN avatar May 17 '23 08:05 JannesN