.@ Tony Finch – blog


This week I am on “escalation engineer” duty, which means triaging new bug reports. One of them was found by ubsan (-fsanitize=undefined) reporting an overflow in a signed integer shift. This was fun to fix because it needed a bit of lateral thinking.

treacherous shifts

C’s bit shift operators require some care to avoid undefined behaviour. One of the pitfalls is that a shift is not allowed to change the sign bit in a signed integer.

It was exactly this problem that ubsan was reporting.

The problem code was in a portability wrapper around setrlimit(), and the bug depended on which platform it was compiled for. The problem code turned out to be 21 years old!

resource limits

BIND has an isc_resourcevalue_t which is always a uint64_t. The portability wrapper needs to convert it to the platform’s rlim_t. It protects against overflow in the conversion by saturating, setting the limit to its maximum value instead.

In principle, that ought to be straightforward, but we don’t know how big rlim_t is (it might be 32 bits or 64 bits) or whether it is signed or not. And there isn’t an RLIM_MAX constant for us to use to check for overflow. We can’t use RLIM_INFINITY because that might be a special out-of-range value like -1.

(POSIX requires rlim_t to be unsigned, but some unixes with long histories have kept it signed for compatibility with old code, and those platforms were where ubsan kicked up a fuss.)

what not to do

The buggy part of the old code was:

	bool rlim_t_is_signed = (((double)(rlim_t)-1) < 0);
	if (rlim_t_is_signed)
		rlim_max = ~((rlim_t)1 << (sizeof(rlim_t) * 8 - 1));

This shifts 1 into the sign bit (a no-no!) then inverts it to get the maximum positive rlim_t value.

fixed version

To avoid the problem, I needed to stay unsigned as much as possible. I can use an isc_resourcevalue_t which is unsigned and (should be!) at least as large as rlim_t. So, instead of asking how big an rlim_t is, the new question is how much bigger is an isc_resourcevalue_t?

We can count how many bytes wider our rlim_max variable is (should be 0 or 4) and whether rlim_t has a sign bit. (I couldn’t think of a better way to work that out.)

	isc_resourcevalue_t rlim_max = UINT64_MAX;
	size_t wider = sizeof(rlim_max) - sizeof(rlim_t);
	bool sign_bit = (double)(rlim_t)-1 < 0;

Then we just need to shift the excess bits out of rlim_max to get the result we want.

	rlim_max >>= CHAR_BIT * wider + sign_bit;

Then we can convert the resource limit value with overflow protection.

	rlim_value = ISC_MIN(value, rlim_max);

bonus

This change replaced 18 lines of code with 8 lines. I like patches with negative net LoC :-)