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 :-)