Map

Passing Too Few Parameters and the Itanium NaT Bit

Wiki summarycalling-conventionsitaniumundefined-behaviorcompilerslow-level โ†ณ show in map Markdown
title
Passing Too Few Parameters and the Itanium NaT Bit
type
summary
summary
Raymond Chen on the failure modes of calling a function with fewer parameters than declared, and how Itanium's register-stack engine architecturally enforces parameter count
tags
calling-conventions, itanium, undefined-behavior, compilers, low-level
created
2026-04-30
updated
2026-04-30

Raymond Chen's April 27 2026 post walks through a deceptively simple question: if a function ignores its second parameter when the first is positive, is it safe to call with only one argument? The C and C++ standards say no โ€” calling a function with the wrong number of parameters is undefined behavior โ€” and the post catalogs the concrete ways that UB manifests on real hardware.

Failure modes on conventional architectures

Stack imbalance (callee-clean conventions). Conventions like stdcall make the callee responsible for popping arguments. If the caller pushes one fewer parameter than the callee expects, the callee pops one extra slot and the stack pointer is now off. Subsequent return addresses, saved registers, and locals all shift, usually leading to memory corruption or a wild jump. See oldnewthing-20260423 (referenced from the post) on uninstaller code injection causing this in Explorer.

Scratch reuse of the unused parameter slot. Even when the conversion convention is caller-clean, the callee assumes the parameter's storage was provided. The compiler may optimize an unrelated dead local into the parameter's slot. Chen's example:

int blah(int a, int b)
{
    if (a <= 0) {
        int c = f1();
        f2(a);
        return c;
    } else {
        return f3(a, b);
    }
}

Here the compiler may decide c and b can share storage because b is dead in the a <= 0 branch โ€” b becomes the slot for c. If the caller didn't reserve memory for b, the assignment to c writes into the calling function's stack frame.

Uninitialized register read. When parameters travel in registers (most modern ABIs), a missing parameter means the callee reads whatever the register happens to contain. On x86-64, ARM64, etc., this manifests as a corrupted value โ€” bad results, but not normally a hardware fault.

What Itanium does differently

Itanium goes further than every other architecture Chen surveys. Two mechanisms:

The NaT ("Not a Thing") bit. Each general-purpose register on Itanium carries an extra bit indicating whether the register holds a valid value. Common ways to enter NaT state are a failed speculative load or a calculation where any input was already NaT. Reading a NaT register doesn't trap by itself โ€” but spilling a NaT to memory raises a "NaT consumption" exception. Chen's example function takes the address of an unused parameter and crashes even when never reading the parameter's value, because the compiler had to spill it to give it an address.

Architectural register stack frames. On Itanium, the function-call ABI is enforced by hardware, not just convention. The caller declares "I am passing N output registers" via the register-stack engine; the callee sees them renumbered starting at r32. Reading a stacked register outside the current frame is undefined; writing one outside the current frame is required to raise an Illegal Operation fault. A leaf function (no custom frame) inherits exactly the input parameter registers and nothing more. If the caller declared two output registers and the callee was compiled expecting three, the third read is officially undefined and the architecture is allowed to fault.

The combination โ€” NaT propagating through speculation plus the hardware stack frame โ€” means that calling an Itanium function with too few parameters can produce a hardware exception out of what looks like a register-to-register move.

The general lesson

The C/C++ standards already say "don't do this." Most architectures degrade gracefully (corrupted values, stack imbalance, eventual segfault). Itanium turned the same UB into a fault at the exact instruction that violated the convention. This is the same pattern as Itanium's other strict-enforcement quirks โ€” the architecture treats convention violations as architectural errors, not as quietly-corrupted state.

It's also one of the reasons Itanium was hard to retrofit existing C/C++ code onto: optimizations that were silent on x86 (parameter-slot reuse, dead-store elimination across calls) became visible faults on Itanium because the architecture cared about details no other CPU enforced.