Signed Overflow in Firmware Counters: Make the Arithmetic Contract Explicit

Check an arithmetic operation before using its result as a size, limit or progress condition. A signed calculation that exceeds its representable range has no portable wraparound result to inspect afterward. If wrap is intentional, choose an explicitly documented unsigned or compiler-specific contract; if wrap is an error, use checked arithmetic and reject the operation before allocating or copying.

This guide covers C11 application arithmetic and GCC 14.2.0. Host probes ran on x86-64 Linux with that compiler. They do not establish MCU performance or vendor-library behavior. RTOS tick rollover and persistent sequence-number comparison are excluded; the engineering target is counters and computed buffer lengths.

Write an expression-level range worksheet

For each risky expression, record the declared operand types, the types after integer promotions and conversions, the maximum mathematical result, the destination type and the failure policy. The expression’s evaluation type matters before assignment. A wide destination cannot rescue an overflow that already occurred in a narrower intermediate.

The WG14 N1570 C11 draft, sections 6.5, 6.3.1.1 and 6.3.1.8, supplies the expression and promotion rules. Section 6.2.5 defines unsigned arithmetic modulo its resulting type’s range. The important qualification is the resulting type: operands declared unsigned can still undergo integer promotion before arithmetic.

Arithmetic review determines promoted expression type, proves range or selects checked arithmetic, rejects overflow before committing a length, and treats intentional modular counters as a separate documented branch.
Prove the intermediate result or detect overflow before committing it. Assignment to a wider destination happens too late to repair a bad intermediate.

For example, on the tested host, two uint16_t operands with value 65535 promote to 32-bit int. Their mathematical product, 4294836225, exceeds INT_MAX. Writing that product into uint32_t afterward does not change the multiplication type. Casting the operands to uint32_t before multiplication produced the expected value in the host fixture, because that particular product fits uint32_t. This is a type-and-range example, not a general instruction to cast every expression to unsigned.

Recognize a test that assumes overflow already happened

The original diagnostic function below is intentionally unsuitable as an overflow detector:

int grows(int x) {
    return x + 1 > x;
}

At INT_MAX, the default C expression overflows. In the executed GCC 14.2.0 O2 host probe, it returned one with ordinary options and zero with -fwrapv. The default result is only an observation of an undefined execution, not a required compiler outcome. Comparing debug and optimized results can reveal a bad assumption, but agreement would not make that assumption valid.

The versioned code-generation manual defines fwrapv for signed addition, subtraction and multiplication. It is a build contract, not a universal safety switch. It does not turn division by zero, invalid shifts, out-of-bounds accesses or oversized allocations into valid operations. Record effective flags for every relevant component if the application deliberately depends on this extension.

Separate calculation from commitment

For a buffer length, success should mean that all arithmetic fit and the product-specific size policy accepted the result. The following original helper was compiled and executed in the host fixture; its output pointer must be valid:

bool bytes_for(size_t n, size_t width, size_t header,
               size_t *out) {
    size_t product, total;
    if (__builtin_mul_overflow(n, width, &product) ||
        __builtin_add_overflow(product, header, &total))
        return false;
    *out = total;
    return true;
}

GCC’s 14.2.0 overflow-builtins documentation defines the operations using an infinite-precision calculation followed by conversion to the pointed-to result type. The Boolean result reports whether that conversion preserved the mathematical value. The builtins write their result object even when reporting overflow, so this helper uses temporary objects and updates the caller’s output only after both operations succeed.

The fixture checked ordinary products, zero records with a header, multiplication overflow, addition overflow and the largest accepted total. Failure preserved a sentinel output value. It also checked signed addition at INT_MAX minus one and overflow at INT_MAX, plus subtraction below INT_MIN. These defined tests were separate from the intentionally invalid comparison probe.

Apply a product limit after arithmetic succeeds

A mathematically representable length can still exceed the available arena, protocol maximum or downstream API’s argument range. Check those limits before allocation. If another API takes an int or a narrower unsigned type, validate that conversion separately rather than truncating the accepted size.

Consider a hypothetical capture service receiving a record count and record width from configuration. It first validates that each field is permitted, computes header plus count times width with checked operations, then compares the total with its reserved buffer capacity. Only afterward does it publish the new configuration. A failed calculation leaves the previous configuration intact and returns a specific rejection.

This order makes the acceptance evidence clear: a rejected request never becomes an allocation length or loop bound. By contrast, converting a negative count to size_t first can create a huge positive value that obscures the original validation failure. Preserve signed-domain checks before crossing into size arithmetic.

Choose a counter policy deliberately

  • Reject: return an error when another increment cannot be represented.
  • Saturate: retain the maximum and report that precision was lost, if that fits the application.
  • Wrap: use a documented modular representation only when every consumer understands it.
  • Widen: increase range after checking storage, interfaces and the lifetime over which overflow remains possible.

Unsigned wrap is defined arithmetic, but it may still violate a buffer-sizing or progress invariant. A wrapped count that becomes small can be more dangerous than an obvious failure. Keep representability and application correctness as separate review questions.

Qualification checklist

Test zero, one, the largest accepted value, the first rejected value and combinations where multiplication fits but the later addition fails. Preserve output state on rejection. Inspect narrowing conversions and the actual promoted types, then rebuild with production optimization. Repeat against the selected target compiler before release; the host results reported here establish no cycle count or MCU code-size claim.

The final deliverable is a range worksheet paired with explicit rejection, saturation or modular behavior. It should explain what happens at the boundary without relying on a signed overflow result that the language never promised.

Similar Posts