skip to content

Your permissions service outgrows 31 flags in a 32-bit mask — what breaks, and how do you fix it?

level: seniorimportance: should knowfreq 32%

answer

  1. count the usable positions in the word
  2. one bit is not like the others
  3. what does shifting past the width really do
  4. the new flag may not be new
  5. widening a negative value fills the top

basics

~20 s

Flag 31 lands on the sign bit, so masks turn negative. Past that, shifting by 32 or more is not a reliable zero: many environments reduce the count modulo the width, so flag 32 silently aliases flag 0.

solid answer

~50 s

A 32-bit signed value holds 31 comfortable flag positions plus the sign bit. Bit 31 makes the mask negative, which surprises range checks, sign-copying right shifts, and any storage or wire format assuming a non-negative value. Bit 32 is worse: the shift count is commonly reduced modulo the word width, so `1 << 32` evaluates to 1 and the new permission becomes an exact alias of flag 0 — granting one silently grants the other, with no error raised. Widening a stored negative mask into a 64-bit field by sign extension then sets bits 32 through 63, handing out permissions nobody assigned. Fix it by widening through an explicitly zero-extending path, or better, by moving to an array of words behind a small set abstraction so capacity stops being a machine limit — and migrate persisted masks under a version marker.

go deeper

for a junior

Know that a 32-bit mask holds a fixed number of flag positions and that the top one is the sign bit. Recognising that the capacity is finite, and roughly where it ends, is the expectation here.

for a middle

Explain why using the top bit makes the value negative and what that does to range checks and sign-copying right shifts. Say plainly that a shift count at or beyond the operand width is not a dependable zero.

for a senior

Diagnose the aliasing failure: a new flag that silently equals an existing one grants permissions nobody assigned, with nothing raised. Handle the widening migration's sign-extension trap and add a capacity guard where flags are registered.

for a principal

Own the choice between widening the word, moving to an array of words, or changing representation outright, and the migration policy that comes with it: version markers on stored values, idempotent backfills, and a standing ban on reusing retired positions.

## The capacity you actually have A mask stored in a 32-bit signed integer has 32 bit positions, numbered 0 through 31, and the top one is the sign bit. So there are 31 comfortable flag positions and one that works arithmetically but changes how the value behaves everywhere else. This is the boundary an access-control service walks into after a few years of feature growth, and it is worth knowing the two distinct failures on either side of it: the sign-bit problem *at* 31, and the aliasing problem *past* 31. ## Failure one: the sign bit Setting bit 31 makes the two's-complement value negative. The bitwise operations themselves are unaffected — AND, OR, NOT and XOR are pure bit operations and do not care about the interpretation — so grant, revoke and test keep working. What breaks is everything around them: - **Ordering and range checks.** A mask that is now negative fails a `mask > 0` sanity check or a "must be non-negative" validation, and sorts before every other mask. - **Right shift.** Arithmetic right shift copies the sign bit downward, so shifting a negative mask right fills with ones rather than zeros. Code that walks bits by shifting right until the value reaches zero loops forever. - **Storage and transport.** A schema column, a serialization format, or a client that assumed the mask fits an unsigned range now sees a large negative number, and a text log shows something like -2147483648 where an operator expected a positive integer. None of these are subtle to fix once identified; they are hard to *find* because the permission logic itself still passes its tests. ## Failure two: shifting past the width This is the dangerous one. When the flag list grows past 32 entries and someone writes `1 << 32` against a 32-bit value, the intuitive expectation is "the bit shifts off the end and I get 0". That is not what most environments do. It is common for the shift count to be reduced modulo the operand width, which makes `1 << 32` equal to `1 << 0` = 1. The 33rd permission is then bit-for-bit identical to the first one. Granting the new permission grants the old one; revoking either revokes both; an authorization check for the first passes for anyone holding the new one. There is no exception, no overflow flag, no test failure — only an authorization bug that looks like a data problem. Runtimes genuinely differ here, which is why "it worked on my machine" is not evidence: some mainstream environments define the shift count as taken modulo the width (Java and JavaScript both reduce a 32-bit shift count to its low five bits), C and C++ leave a shift at or beyond the width undefined so the compiler may produce anything including a constant folded at build time, and Python's arbitrary-precision integers simply keep growing so `1 << 32` is a genuinely larger number with no wraparound at all. The same source line has three different meanings. Treat any shift whose count is not provably less than the operand width as a defect. ## Failure three: the widening migration The obvious remedy is to move the field to 64 bits. Done carelessly, this is its own outage. If the stored 32-bit mask is negative (bit 31 set) and the conversion to the wider type is *sign-extending*, the value acquires ones in bits 32 through 63 — thirty-two permissions granted to everyone whose mask happened to use the top flag. The migration must convert through an explicitly zero-extending path, or mask the old value down to its low 32 bits before widening. ## The fixes, in increasing order of commitment 1. **Widen to 64 bits.** Cheapest, buys 63 more safe positions, and postpones the problem rather than removing it. Requires the zero-extension care above and a migration of every stored value. 2. **Split into an array of words.** Represent the set as a fixed array of 64-bit words with a helper that maps flag index to `(word = index / 64, bit = index % 64)`. Union and intersection become short loops over the words — still very fast, still cache-friendly, and now the capacity is a configuration number rather than a machine limit. Any single-word optimization is gone, but authorization checks that touch one flag still touch one word. 3. **Change the representation.** Move to an explicit set of named permission values, and keep the mask, if at all, as an internal cache derived from the names on the hot path. This is the right call when the permission list is growing steadily rather than asymptoting, and it is discussed as a design decision in its own right. ## Migration discipline Whatever the target, persisted masks are the hard part. Bit positions are a contract with every stored row and every client that already holds a mask, so the migration needs a version marker on the stored value or the schema — never a silent reinterpretation of the old column. Write the backfill so it is idempotent and re-runnable, verify a sample of rows decodes to the same permission *names* before and after, and add a guard that refuses to register a flag index at or beyond the representation's capacity. That last check is three lines and turns the aliasing failure from a security incident into a startup error.

  • Why is the aliasing failure worse than an overflow that throws?
    Because nothing signals it. A thrown error stops the request and names the line; an aliased flag returns a plausible answer, so the first symptom is a user holding a permission nobody granted, discovered by audit rather than by monitoring. The defensive move is a registration-time assertion that every flag index is strictly below the representation's capacity.
  • You widen the column to 64 bits. What must the backfill do to old rows with the top flag set?
    Convert through a zero-extending path, or mask the old value to its low 32 bits before widening. A sign-extending conversion of a negative 32-bit mask sets bits 32 through 63, granting thirty-two unassigned permissions to every affected principal. Verify by decoding a sample of rows to permission names before and after and diffing the name sets.
  • When would you jump straight to an array of words rather than widening to 64 bits?
    When the flag list is still growing at a steady rate, so 64 only buys a couple more years and you would pay the migration twice. An array of words behind a small set abstraction makes capacity a configuration value: union and intersection become short word-wise loops, single-flag checks still touch one word, and no future growth needs another schema change.

saying these in an interview costs you the question

  • Assumes shifting past the word width reliably yields zero
  • Believes a 32-bit signed mask holds 32 safe flag positions
  • Widens the stored mask without checking for sign extension
  • Says the bitwise operators themselves break at the sign bit
  • Reinterprets stored masks under a new bit layout with no version marker
  • Reuses a retired flag position to reclaim capacity

context