C Basics · beginner · ~12 min

Bitmasks and flags

Pack many booleans into one integer.

Overview

A single integer can carry a whole set of independent yes/no facts, one per bit. Name each bit (READ, WRITE, EXEC) and the integer becomes a flag word: OR combines flags, AND tests them, and AND-NOT revokes them. The important refinement over single-bit work is that a mask may cover several bits at once, which raises a question the single-bit case hides — when you test a multi-bit mask, do you mean all of these flags or any of them? (v & m) == m asks all; (v & m) != 0 asks any. Choosing the wrong one is the classic flag-word bug.

Why it matters

Flag words are how C APIs accept options without exploding into a dozen boolean parameters: open() takes O_RDONLY | O_CREAT | O_TRUNC, mmap() takes protection and mapping flags, a TCP header packs SYN/ACK/FIN into one byte. Reading and writing these correctly is a daily task in systems code, and the all-versus-any distinction decides whether a permission check actually holds.

Core concepts

Defining flags. Give each flag a distinct single bit: enum { READ = 1u<<0, WRITE = 1u<<1, EXEC = 1u<<2 };. Powers of two guarantee the flags never overlap, so they can be combined and separated losslessly.

Combining. unsigned perm = READ | WRITE; — OR unions the sets. Because the bits are distinct, no information is lost.

Testing any. (v & m) != 0 is true when at least one flag in m is present. Use it for "does this have any write-ish permission?".

Testing all. (v & m) == m is true only when every flag in m is present. Use it for "does this have both read and write?". For a single-bit mask the two forms agree, which is exactly why the bug hides until someone passes a two-bit mask.

Revoking. v &= ~m clears every flag in m at once.

Toggling a set. v ^= m flips each flag in m — occasionally useful, but rarely what a permission update wants.

Syntax notes

enum { READ = 1u << 0, WRITE = 1u << 1, EXEC = 1u << 2 };

unsigned perm = READ | WRITE;        // combine

if ((perm & WRITE) != 0)  { /* has write */ }              // ANY
if ((perm & (READ|WRITE)) == (READ|WRITE)) { /* has BOTH */ }  // ALL

perm &= ~WRITE;                     // revoke write
perm |= EXEC;                       // grant exec

Key points:

  • (v & m) == m is "all of m"; (v & m) != 0 is "any of m". They differ the moment m has more than one bit.
  • Parenthesise the comparison: v & m == m parses as v & (m == m), which is always v & 1.
  • Use an enum or #define so the flag names appear in the code, not raw numbers.

Lesson

A single integer can hold dozens of on/off flags. A mask names a group of them. (v & mask) == mask tests that all are present; v & ~mask clears a group; v | mask sets one.

The demo models rwx permissions in three bits.

Code examples

#include <stdio.h>
enum { READ=1u<<0, WRITE=1u<<1, EXEC=1u<<2 };
static int has_all_flags(unsigned v,unsigned m){ return (v&m)==m?1:0; }
static unsigned clear_flags(unsigned v,unsigned m){ return v & ~m; }
int main(void){
    unsigned perm = READ | WRITE;
    printf("rwx = %c%c%c\n",
        has_all_flags(perm,READ)?'r':'-',
        has_all_flags(perm,WRITE)?'w':'-',
        has_all_flags(perm,EXEC)?'x':'-');
    perm = clear_flags(perm, WRITE);        /* revoke write */
    printf("has write after revoke? %s\n", has_all_flags(perm,WRITE)?"yes":"no");
    return 0;
}

Line by line

Step Line What happens
1 enum { READ=1u<<0, WRITE=1u<<1, EXEC=1u<<2 } Values 1, 2, 4 — one distinct bit each.
2 unsigned perm = READ | WRITE; 0000 0011 — both flags present in one integer.
3 has_all_flags(perm, READ) (0b011 & 0b001) == 0b001 → true, prints r.
4 has_all_flags(perm, EXEC) (0b011 & 0b100) is 00b100 → false, prints -.
5 clear_flags(perm, WRITE) perm & ~0b0100b001; read survives, write is gone.
6 re-test WRITE Now false — the revoke took effect without touching READ.

Common mistakes

Testing v & mask (any flag) when you meant (v&mask)==mask (all flags).

Debugging tips

Compiler errors and warnings:

  • warning: suggest parentheses around comparison in operand of '&' — you wrote v & m == m. This is the single most valuable warning in this lesson; fix it, do not silence it.
  • warning: enumerated and non-enumerated type in conditional expression — mixing an enum with unsigned; cast or declare the flags as unsigned constants.

Runtime symptoms:

  • A permission check passes when only one of two flags is set. You used the ANY form (v & m) != 0 where you meant ALL, (v & m) == m.
  • Every check returns true. Precedence — v & m == m collapsed to v & 1.
  • Two flags interfere. Two of your flag constants share a bit (for example 4 and 6 overlap). Flags must be distinct powers of two.
  • Revoking clears everything. You wrote v &= m instead of v &= ~m.

Technique: print the flag word in binary alongside the mask; overlapping or mis-numbered flags become obvious immediately.

Memory safety

Flag words are values, so the risks are logical rather than memory-related — but in security code a logic slip is the vulnerability:

  • Fail-closed on permission checks. Prefer the ALL form for authorisation: requiring every needed bit means an unexpected value denies access rather than granting it.
  • Distinct powers of two. If two flags share a bit, granting one silently grants the other. Define them as 1u << n with unique n, and consider a compile-time assertion that they do not overlap.
  • Untrusted flag words. When flags arrive from input, mask off the bits you actually support (v &= SUPPORTED_MASK) before acting, so unknown bits cannot reach code that does not expect them.
  • Width. A flag word with more than 31 flags needs uint64_t; 1u << 32 is undefined behaviour, not a 33rd flag.

Real-world uses

Concrete uses: open(path, O_WRONLY | O_CREAT | O_TRUNC) and mmap's PROT_READ | PROT_WRITE are flag words. TCP packs SYN, ACK, FIN, RST into a flags field. File modes carry read/write/execute per user, group and other. A window toolkit passes style flags; a compression library passes option bits; a kernel's page tables mark present/writable/user bits.

Professional best practices:

Beginner:

  • Define flags as 1u << n in an enum, never as raw literals scattered through the code.
  • Wrap the tests in named helpers (has_all, has_any) so the intent is explicit at every call site.

Intermediate:

  • Choose ALL semantics for permission checks; reserve ANY for "is anything set?" queries.
  • Keep a SUPPORTED_MASK and reject or strip unknown bits from external input.
  • Document each flag's meaning next to its definition — a flag word is only as readable as its names.

Practice tasks

1. (Beginner) Define and combine. Declare READ, WRITE, EXEC as bit flags and build the value for read+execute. Print it in binary. Example: r-x101. Concepts: 1u << n, OR.

2. (Beginner) any vs all. Implement int has_any(unsigned v, unsigned m) and int has_all(unsigned v, unsigned m). Requirements: demonstrate a case where they disagree. Example: v=READ, m=READ|WRITE → any 1, all 0. Concepts: the two test forms.

3. (Intermediate) Permission string. Write void to_string(unsigned perm, char out[4]) producing "rwx", "r-x", "---". Concepts: per-flag testing, output formatting.

4. (Intermediate) Sanitise input. Write unsigned sanitize(unsigned v) that strips any bit outside READ|WRITE|EXEC. Requirements: an input of 0xFF must yield only the supported bits. Concepts: supported-mask, defensive input handling.

Summary

Give each flag a distinct power-of-two bit and one integer carries a whole set of options: OR combines, &= ~m revokes, and testing splits into two distinct questions — (v & m) == m for all of the flags and (v & m) != 0 for any. Those two agree for a single-bit mask and diverge for a multi-bit one, which is why permission checks should use the ALL form and fail closed. Name your flags, parenthesise your comparisons, and strip unsupported bits from anything that arrives as input.

Practice with these exercises