Skip to content

Generated temporaries shadow user globals — two verified silent miscompiles (_<var>_len, _tmp<N>) #1131

Description

@jlaustill

Summary

Two families of transpiler-invented temporaries are emitted with names a user is allowed to declare. When the names coincide, the generated temporary shadows the user's variable and every subsequent read silently binds to the wrong storage.

Both produce wrong runtime values with gcc -std=c99 -Wall -Wextra completely clean and the transpiler exiting 0. This is the same failure class as #1117 collision class 2 — the silent one — in a family ADR-063 did not cover.

Family Built at Verified result
_<var>_len (strlen cache) CodeGenerator.ts:1293`_${varName}_len` expected 47, got 10
_tmp<N> (slice-assignment unroll) CodeGenState.ts:1247`_tmp${this.tempVarCounter++}` expected 7, got 0

Reproduction 1 — _<var>_len

u8 _msg_len <- 42;

u8 count() {
    string<16> msg <- "hello";
    u8 total <- 0;
    if (msg.char_count > 2) {
        total <- msg.char_count + _msg_len;
    }
    return total;
}

u32 main() { return (u32)count(); }

Generated:

uint8_t _msg_len = 42U;          /* the user's global */

uint8_t count(void) {
    uint8_t total = 0U;
    size_t _msg_len = strlen(msg);   /* shadows it */
    if (_msg_len > 2) {
        total = _msg_len + _msg_len; /* source said: msg.char_count + _msg_len */
    }
    return total;
}

Look at the emitted expression: total = _msg_len + _msg_len. The source was msg.char_count + _msg_len, i.e. 5 + 42 = 47. Both operands now resolve to the shadowing local.

$ gcc -std=c99 -Wall -Wextra -o lensilent lensilent.c     # no diagnostics at all
$ ./lensilent; echo $?
10                                                        # expected 47

Reproduction 2 — _tmp<N>

u8[4] buffer;
u8 _tmp0 <- 7;

void store(u32 magic) {
    buffer[0, 4] <- magic;
    buffer[0] <- buffer[0] + _tmp0;
}

u32 main() {
    store(0);
    return (u32)buffer[0];
}

Generated — the MISRA 21.15 slice unroll introduces _tmp0 inside the function:

uint8_t _tmp0 = 7U;                              /* the user's global */

void store(uint32_t magic) {
    const uint32_t _tmp0 = (uint32_t)(magic);    /* shadows it */
    buffer[0] = (uint8_t)(_tmp0);
    ...
    buffer[0] = buffer[0U] + _tmp0;              /* adds magic, not 7 */
}
$ gcc -std=c99 -Wall -Wextra -o tmpsilent tmpsilent.c     # no diagnostics at all
$ ./tmpsilent; echo $?
0                                                         # expected 7

Both verified on main @ 1733ba70.

Why nothing catches it

  • -Wall -Wextra do not include -Wshadow. With -Wshadow gcc does report "declaration of '_msg_len' shadows a global declaration" — but that flag is not used by this project's pipeline and is not on by default anywhere.
  • E0201 does not apply: _msg_len and _tmp0 contain no __ and no trailing _, so they are perfectly legal C-Next identifiers. A leading underscore is explicitly legal under ADR-063.
  • scripts/batch-validate.mjs only ever runs over this repo's own generated tests/ output, never over a user's project, so it could not catch a user-side collision regardless.

Proposed fix — make it unrepresentable, not diagnosed

Both names are purely invented — no user-visible contract depends on their spelling. So they can simply be moved into a shape E0201 already forbids in user source:

// CodeGenerator.ts:1293
- const tempVar = `_${varName}_len`;
+ const tempVar = QualifiedCName.join("cnx", "len", varName);   // cnx__len__msg

// CodeGenState.ts:1247
- return `_tmp${this.tempVarCounter++}`;
+ return QualifiedCName.join("cnx", `tmp${this.tempVarCounter++}`);  // cnx__tmp0

Because a user identifier may not contain __, cnx__len__msg and cnx__tmp0 are unrepresentable in C-Next source. That closes the family structurally, with no new diagnostic and no new rule — reusing the guarantee ADR-063 already bought. It is strictly better than reserving a prefix, which would only turn a silent miscompile into an error message.

The same treatment applies to _cnx_tmp_<N> (ArgumentGenerator.ts:118), which has the same shape and is not covered by a cnx_ prefix rule anyway because it starts with an underscore.

Note this requires regenerating snapshots — _tmp0_tmp5 and _<var>_len appear in committed expected output.

Related

  • Scope name mangling collides with the global namespace, producing non-compiling C #1117 / ADR-063 — same collision class, different family. ADR-063 fixed scope-qualified names and explicitly scoped itself there; these invented temporaries were never in scope.
  • The cnx_ prefix reservation issue — these two families are listed there as gaps the prefix rule does not close, because a prefix ban would produce a diagnostic where this fix produces impossibility.

Acceptance

  • _<var>_len and _tmp<N> (and _cnx_tmp_<N>) are renamed into a __-containing shape.
  • Both reproductions above become impossible to express rather than merely rejected.
  • Execution regression tests for both, asserting the runtime values (47 and 7), not just that they compile — a compile-only test would pass today.
  • Snapshots regenerated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcode-generatorIssues in code generation/transpilationpriority: highHigh priority - critical for v1 or blocks multiple featuressafetySafety features (clamp, wrap, overflow handling)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions