llvm / llvm/llvm-project

Clang miscompiles inline null check with const pointer typedef and global compound literal

Open
#222,501 2 comments 0 reactions 0 assignees View on GitHub
clang
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

> _Prepared with AI_

Summary

I am seeing a possible Clang miscompile where an inline null check enters the true branch even though the same expression evaluates to false immediately before it.

This reproduces with a const pointer typedef pattern plus a global pointer initialized from a compound literal.

Environment

- OS: Linux x86_64
- Compiler 1: Ubuntu clang version 12.0.1-19ubuntu3
- Compiler 2: Latest Clang on Compiler Explorer (Godbolt), also reproduces
- Language mode: C
- Typical flags:
- O0
- g3
- std=c11
- pedantic
- Wall
- Wextra
- fsanitize=address,undefined

Expected behavior

If the expression state_ptr->p == NULL evaluates to 0 in a print right before an if statement, then the if should not enter the null branch.

Actual behavior

The inline if enters the null branch even though the same expression prints as false right before it.

It works as expected when I remove `const` from the `typedef`.

Minimal reproducer (single file, UB-free cleanup, correct percent-p casts)
https://godbolt.org/z/6Gaqo8baY
```c
#include
#include

typedef struct s_state {
int n;
void *p;
} t_state_;

typedef t_state_ *const t_state;

/* Same pattern as your project */
t_state g_state = &(t_state_){0};

static void alloc_once(void)
{
g_state->p = malloc(123); // or any non-zero value: (void *)123123;
}

/* Test 1: cached bool */
static int test_cached_bool(void)
{
int is_null;

alloc_once();
is_null = (g_state->p == NULL);
printf("[cached-bool] p=%p is_null=%d\n", g_state->p, is_null);

if (is_null) {
printf("[cached-bool] entered null branch\n");
return 1;
}
printf("[cached-bool] ok branch\n");
return 0;
}

/* Test 2: inline expr */
static int test_inline_expr(void)
{
alloc_once();
printf("[inline] p=%p eq_null=%d\n", g_state->p, (g_state->p == NULL));

if (g_state->p == NULL) {
printf("[inline] entered null branch\n");
return 1;
}
printf("[inline] ok branch\n");
return 0;
}

/* Test 3: snapshot pointer (volatile read barrier-ish) */
static int test_snapshot_ptr(void)
{
volatile void *snap;

alloc_once();
snap = g_state->p;
printf("[snapshot] snap=%p eq_null=%d\n", (void *)snap, (snap == NULL));

if (snap == NULL) {
printf("[snapshot] entered null branch\n");
return 1;
}
printf("[snapshot] ok branch\n");
return 0;
}

static void cleanup(void)
{
if (g_state->p != NULL) { // not work
printf("cleanup");
free(g_state->p);
g_state->p = NULL;
}
}

int main(void)
{
int r1, r2, r3;

g_state->n = 8;

r1 = test_cached_bool();
cleanup();

r2 = test_inline_expr();
cleanup();

r3 = test_snapshot_ptr();
cleanup();

printf("RESULT cached=%d inline=%d snapshot=%d\n", r1, r2, r3);
return 0;
}
```
Build and run

clang -O0 -g3 -std=c11 -pedantic -Wall -Wextra -fsanitize=address,undefined repro.c -o repro
./repro

Observed output

[cached-bool] p=0x7194713e0010 is_null=0
[cached-bool] ok branch
[inline] p=0x7194713e0030 eq_null=0
[inline] entered null branch
[snapshot] snap=0x7194713e0050 eq_null=0
[snapshot] ok branch
RESULT cached=0 inline=1 snapshot=0

Notes

- The reproducer avoids double free and nulls out freed pointers.
- Percent-p uses explicit void pointer casts.
- Behavior appears expression-form dependent:
- cached bool path behaves as expected
- inline expression path enters wrong branch
- volatile snapshot path behaves as expected
- Same logic does not reproduce for me with GCC (expected behavior there).

Question

Is this a known Clang bug around this pattern, or would you like a reduced preprocessed file and IR/assembly attachments for further triage?

Contributor guide

Open the contributing guide

Research direction

Start with the single-file repro.c and compile it using the listed Clang command, then compare the cached-bool, inline-expression, and volatile-snapshot results. Use the Compiler Explorer reproducer and inspect the generated IR or assembly if needed. Done means confirming and reducing the compiler miscompile sufficiently for actionable triage, with the requested preprocessed file or IR attachments if they clarify the failure.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.