Skip to content

Commit 4937773

Browse files
committed
Add m041 ptxas miscompile reproducer
1 parent bfecc8e commit 4937773

6 files changed

Lines changed: 252 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ Version | Description |
118118
| 13.2.78 | [m038-structured-empty-else-fold](known-miscompiles/m038-structured-empty-else-fold/NOTES.md): Always-false structured branch with an empty else arm folds as if the untaken then arm executed. |
119119
| 13.2.78 | [m039-else-redefinition-fold](known-miscompiles/m039-else-redefinition-fold/NOTES.md): Branch fold drops the executed else-path redefinition of a value initialized before the branch. |
120120
| 13.2.78 | [m040-mulwide-neg-shr-fold](known-miscompiles/m040-mulwide-neg-shr-fold/NOTES.md): `mul.wide` low word feeding wrapped negation and logical shift loses the shifted high-bit contribution. |
121+
| 13.2.78 | [m041-or-shifted-square-fold](known-miscompiles/m041-or-shifted-square-fold/NOTES.md): `or.b32` after a square known to have zero low 32 bits folds with a missing output bit. |
121122

122123
## Running
123124

@@ -209,6 +210,7 @@ that feature.
209210
| `DIV_DISABLE_MULHI` | `mul.hi.u32` and `mul.hi.s32`. |
210211
| `DIV_DISABLE_SIGNED_MULHI` | `mul.hi.s32` only. |
211212
| `DIV_DISABLE_BITWISE_BINOPS` | `and.b32`, `or.b32`, `xor.b32`. |
213+
| `DIV_DISABLE_OR` | `or.b32` while retaining `and.b32` and `xor.b32`. |
212214
| `DIV_DISABLE_XOR` | `xor.b32` while retaining `and.b32` and `or.b32`. |
213215
| `DIV_DISABLE_PRMT` | `prmt.b32`. |
214216
| `DIV_DISABLE_NOT` | `not.b32` and xor-by-`0xffffffff` forms. |

‎crates/fuzzx-diff/src/main.rs‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
//! PTX mul.hi.s32 generation while retaining mul.hi.u32
3333
//! DIV_DISABLE_BITWISE_BINOPS default: false; set 1/true/yes/on to suppress
3434
//! PTX and.b32/or.b32/xor.b32 generation
35+
//! DIV_DISABLE_OR default: false; set 1/true/yes/on to suppress
36+
//! PTX or.b32 generation while retaining and.b32/xor.b32
3537
//! DIV_DISABLE_XOR default: false; set 1/true/yes/on to suppress
3638
//! PTX xor.b32 generation while retaining and.b32/or.b32
3739
//! DIV_DISABLE_PRMT default: false; set 1/true/yes/on to suppress
@@ -191,6 +193,8 @@ struct Args {
191193
#[arg(long)]
192194
disable_bitwise_binops: bool,
193195
#[arg(long)]
196+
disable_or: bool,
197+
#[arg(long)]
194198
disable_xor: bool,
195199
#[arg(long)]
196200
disable_prmt: bool,
@@ -293,6 +297,7 @@ impl Args {
293297
set_bool!(self.disable_mulhi, "DIV_DISABLE_MULHI");
294298
set_bool!(self.disable_signed_mulhi, "DIV_DISABLE_SIGNED_MULHI");
295299
set_bool!(self.disable_bitwise_binops, "DIV_DISABLE_BITWISE_BINOPS");
300+
set_bool!(self.disable_or, "DIV_DISABLE_OR");
296301
set_bool!(self.disable_xor, "DIV_DISABLE_XOR");
297302
set_bool!(self.disable_prmt, "DIV_DISABLE_PRMT");
298303
set_bool!(self.disable_not, "DIV_DISABLE_NOT");
@@ -394,6 +399,7 @@ impl Config {
394399
let disable_mulhi = env_bool("DIV_DISABLE_MULHI")?.unwrap_or(false);
395400
let disable_signed_mulhi = env_bool("DIV_DISABLE_SIGNED_MULHI")?.unwrap_or(false);
396401
let disable_bitwise_binops = env_bool("DIV_DISABLE_BITWISE_BINOPS")?.unwrap_or(false);
402+
let disable_or = env_bool("DIV_DISABLE_OR")?.unwrap_or(false);
397403
let disable_xor = env_bool("DIV_DISABLE_XOR")?.unwrap_or(false);
398404
let disable_prmt = env_bool("DIV_DISABLE_PRMT")?.unwrap_or(false);
399405
let disable_not = env_bool("DIV_DISABLE_NOT")?.unwrap_or(false);
@@ -435,6 +441,7 @@ impl Config {
435441
emit_mulhi: !disable_mulhi,
436442
emit_signed_mulhi: !disable_signed_mulhi,
437443
emit_bitwise_binops: !disable_bitwise_binops,
444+
emit_or: !disable_or,
438445
emit_xor: !disable_xor,
439446
emit_prmt: !disable_prmt,
440447
emit_not: !disable_not,
@@ -646,7 +653,7 @@ fn main() -> Result<()> {
646653

647654
let total_workers = cfg.gpus.len() * cfg.workers_per_gpu;
648655
eprintln!(
649-
"fuzzx-diff: starting_seed=0x{:016x} out={} program_bytes={} max_iters={} control_flow={:?} blocks={}..{} insts_per_block={}..{} regs={} max_loop_iters={} max_immediate={} max_structured_depth={} emit_structured_loops={} emit_arbitrary_loops={} emit_lop3={} emit_minmax={} emit_sub={} emit_mulhi={} emit_signed_mulhi={} emit_bitwise_binops={} emit_xor={} emit_prmt={} emit_not={} emit_clz={} emit_cnot={} emit_abs={} emit_signed_cmp={} emit_signed_divrem={} emit_funnel={} emit_neg={} emit_shl={} emit_signed_shr={} emit_bfind={} emit_bfi={} emit_bmsk={} emit_mad24={} emit_mul24={} emit_mul_wide={} emit_wide_int={} emit_addc={} emit_subc={} emit_i32_boundary_immediates={} emit_dp2a={} emit_set={} emit_s32_slct={} emit_video={} emit_vsub4={} gpus={:?} workers_per_gpu={} (total={})",
656+
"fuzzx-diff: starting_seed=0x{:016x} out={} program_bytes={} max_iters={} control_flow={:?} blocks={}..{} insts_per_block={}..{} regs={} max_loop_iters={} max_immediate={} max_structured_depth={} emit_structured_loops={} emit_arbitrary_loops={} emit_lop3={} emit_minmax={} emit_sub={} emit_mulhi={} emit_signed_mulhi={} emit_bitwise_binops={} emit_or={} emit_xor={} emit_prmt={} emit_not={} emit_clz={} emit_cnot={} emit_abs={} emit_signed_cmp={} emit_signed_divrem={} emit_funnel={} emit_neg={} emit_shl={} emit_signed_shr={} emit_bfind={} emit_bfi={} emit_bmsk={} emit_mad24={} emit_mul24={} emit_mul_wide={} emit_wide_int={} emit_addc={} emit_subc={} emit_i32_boundary_immediates={} emit_dp2a={} emit_set={} emit_s32_slct={} emit_video={} emit_vsub4={} gpus={:?} workers_per_gpu={} (total={})",
650657
cfg.starting_seed,
651658
cfg.out_dir.display(),
652659
cfg.program_bytes,
@@ -670,6 +677,7 @@ fn main() -> Result<()> {
670677
cfg.gen_config.emit_mulhi,
671678
cfg.gen_config.emit_signed_mulhi,
672679
cfg.gen_config.emit_bitwise_binops,
680+
cfg.gen_config.emit_or,
673681
cfg.gen_config.emit_xor,
674682
cfg.gen_config.emit_prmt,
675683
cfg.gen_config.emit_not,

‎crates/fuzzx-execgen/src/lib.rs‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub struct GenConfig {
5959
pub emit_mulhi: bool,
6060
pub emit_signed_mulhi: bool,
6161
pub emit_bitwise_binops: bool,
62+
pub emit_or: bool,
6263
pub emit_xor: bool,
6364
pub emit_prmt: bool,
6465
pub emit_not: bool,
@@ -117,6 +118,7 @@ impl Default for GenConfig {
117118
emit_mulhi: true,
118119
emit_signed_mulhi: true,
119120
emit_bitwise_binops: true,
121+
emit_or: true,
120122
emit_xor: true,
121123
emit_prmt: true,
122124
emit_not: true,
@@ -1029,6 +1031,7 @@ impl<'a> Generator<'a> {
10291031
self.cfg.emit_mulhi,
10301032
self.cfg.emit_signed_mulhi,
10311033
self.cfg.emit_bitwise_binops,
1034+
self.cfg.emit_or,
10321035
self.cfg.emit_xor,
10331036
)?;
10341037
Ok(Inst::Bin {
@@ -1821,14 +1824,18 @@ fn pick_binop(
18211824
emit_mulhi: bool,
18221825
emit_signed_mulhi: bool,
18231826
emit_bitwise_binops: bool,
1827+
emit_or: bool,
18241828
emit_xor: bool,
18251829
) -> Result<BinOp> {
18261830
let mut ops = vec![BinOp::Add, BinOp::Mul];
18271831
if emit_sub {
18281832
ops.push(BinOp::Sub);
18291833
}
18301834
if emit_bitwise_binops {
1831-
ops.extend([BinOp::And, BinOp::Or]);
1835+
ops.push(BinOp::And);
1836+
if emit_or {
1837+
ops.push(BinOp::Or);
1838+
}
18321839
if emit_xor {
18331840
ops.push(BinOp::Xor);
18341841
}
@@ -2415,6 +2422,29 @@ mod tests {
24152422
assert!(saw_or, "sample did not retain or.b32 coverage");
24162423
}
24172424

2425+
#[test]
2426+
fn or_generation_can_be_disabled() {
2427+
let cfg = GenConfig {
2428+
emit_or: false,
2429+
..GenConfig::default()
2430+
};
2431+
2432+
let mut saw_and = false;
2433+
let mut saw_xor = false;
2434+
for seed in 0..512 {
2435+
let bytes = bytes_from_seed(seed, 4096);
2436+
let ptx = generate_from_bytes_with_config(&bytes, &cfg).unwrap();
2437+
assert!(
2438+
!has_mnemonic(&ptx, "or.b32"),
2439+
"seed {seed:x} emitted or.b32"
2440+
);
2441+
saw_and |= has_mnemonic(&ptx, "and.b32");
2442+
saw_xor |= has_mnemonic(&ptx, "xor.b32");
2443+
}
2444+
assert!(saw_and, "sample did not retain and.b32 coverage");
2445+
assert!(saw_xor, "sample did not retain xor.b32 coverage");
2446+
}
2447+
24182448
#[test]
24192449
fn prmt_generation_can_be_disabled() {
24202450
let cfg = GenConfig {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# m041-or-shifted-square-fold
2+
3+
Found while continuing structured control-flow fuzzing after suppressing the
4+
known `not.b32`, `xor.b32`, `bfind.u32`, `mul24.*`, `mul.hi.*`,
5+
`mul.wide.*`, `bfi.b32`, `bmsk.clamp.b32`, `dp2a.*`, and `mad24.*` families
6+
from earlier runs.
7+
8+
The original saved fuzzer program was:
9+
10+
```text
11+
/tmp/fuzzx-bg-after-m040-20260515T222208Z/div-1778883798-18afdd55cede0677
12+
```
13+
14+
The minimized PTX in `reduced.ptx` keeps the fuzzer input-buffer load, but the
15+
reduced value chain makes the final squared value's low 32 bits zero for all
16+
threads and inputs.
17+
18+
## Scalar Trace
19+
20+
The key chain is:
21+
22+
```text
23+
%r9 = 0x02000000
24+
%r0 = %r2 * %r9
25+
%r1 = 0xff00ff00 & %tid.x
26+
%r3 = %r0 * 16 + %r1
27+
%r1 = 620912 - %r3
28+
%r19 = 0x00080000 * %r1
29+
%r0 = %r19 * %r19
30+
%r0 = %r0 | 0x0000d7cc
31+
```
32+
33+
Because `%r19` is always a multiple of `2^19`, `%r19 * %r19` is a multiple of
34+
`2^38` and its low 32 bits are zero. The final `or.b32` should therefore
35+
store `0x0000d7cc` for every thread. Optimized ptxas stores `0x000097cc`,
36+
clearing bit `0x4000`.
37+
38+
The generated optimized SASS collapses the final value to an `IMAD` immediate
39+
form using `0x97cc`, which points to an integer-combiner fold of the final
40+
`or.b32` after the shifted square.
41+
42+
CUDA inline-PTX repro: `repro_nvcc_inline_ptx.cu`. Build the same source
43+
with `nvcc -Xptxas -O0` and `nvcc -Xptxas -O2`, run both binaries, and
44+
compare the printed output.
45+
46+
This reproduced on 2026-05-15 with CUDA Toolkit 13.2 Update 1 ptxas, the
47+
latest NVIDIA CUDA Toolkit listed on NVIDIA's CUDA Toolkit Archive on
48+
2026-05-15:
49+
50+
```text
51+
release 13.2, V13.2.78
52+
cuda_13.2.r13.2/compiler.37668154_0
53+
```
54+
55+
For continued fuzzing past this family, use `DIV_DISABLE_OR=1`; the reduced
56+
testcase specifically needs the final generated `or.b32`.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
.version 8.8
2+
.target sm_103
3+
.address_size 64
4+
5+
.visible .entry fuzz_kernel(
6+
.param .u64 in_ptr,
7+
.param .u64 out_ptr,
8+
.param .u32 in_n
9+
)
10+
{
11+
.reg .b32 %r<22>;
12+
.reg .b64 %rd<8>;
13+
14+
ld.param.u64 %rd0, [in_ptr];
15+
ld.param.u64 %rd1, [out_ptr];
16+
mov.u32 %r20, %tid.x;
17+
cvta.to.global.u64 %rd2, %rd0;
18+
mul.wide.u32 %rd3, %r20, 4;
19+
add.s64 %rd2, %rd2, %rd3;
20+
ld.global.u32 %r2, [%rd2];
21+
22+
mov.u32 %r9, 33554432;
23+
mul.lo.u32 %r0, %r2, %r9;
24+
and.b32 %r1, 4278255360, %r20;
25+
mad.lo.u32 %r3, %r0, 16, %r1;
26+
sub.u32 %r1, 620912, %r3;
27+
mul.lo.u32 %r19, 524288, %r1;
28+
mul.lo.u32 %r0, %r19, %r19;
29+
or.b32 %r0, %r0, 55244;
30+
31+
exit:
32+
cvta.to.global.u64 %rd4, %rd1;
33+
mul.wide.u32 %rd5, %r20, 16;
34+
add.s64 %rd4, %rd4, %rd5;
35+
st.global.u32 [%rd4 + 0], %r0;
36+
ret;
37+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// CUDA inline-PTX variant of the m041-or-shifted-square-fold ptxas reproducer.
2+
//
3+
// Build this same CUDA source twice and compare the printed output from the -O0 and -O2 binaries:
4+
//
5+
// nvcc -std=c++17 -O2 -arch=sm_103 -Xptxas -O0 \
6+
// repro_nvcc_inline_ptx.cu -o repro_nvcc_inline_ptx_o0
7+
//
8+
// nvcc -std=c++17 -O2 -arch=sm_103 -Xptxas -O2 \
9+
// repro_nvcc_inline_ptx.cu -o repro_nvcc_inline_ptx_o2
10+
//
11+
// Verified on 2026-05-15 with CUDA Toolkit 13.2.1 nvcc/ptxas
12+
// (`release 13.2, V13.2.78`, build `cuda_13.2.r13.2/compiler.37668154_0`).
13+
14+
#include <cuda_runtime.h>
15+
16+
#include <cstdint>
17+
#include <cstdio>
18+
#include <cstdlib>
19+
20+
constexpr int kThreads = 32;
21+
constexpr int kInputWords = 32;
22+
constexpr int kOutputWords = 128;
23+
constexpr uint32_t kN = 32u;
24+
constexpr uint32_t kX = 0x00000000u;
25+
constexpr uint32_t kInput0 = 0x691a38a3u;
26+
constexpr uint32_t kSentinel = 0xa5a5a5a5u;
27+
28+
static void check(cudaError_t err, const char* what) {
29+
if (err != cudaSuccess) {
30+
std::fprintf(stderr, "%s: %s\n", what, cudaGetErrorString(err));
31+
std::exit(2);
32+
}
33+
}
34+
35+
__global__ void repro_kernel(const uint32_t* in, uint32_t* out, uint32_t n, uint32_t x) {
36+
asm volatile(
37+
"{\n\t"
38+
"\n\t"
39+
".reg .b32 r<22>;\n\t"
40+
".reg .b64 rd<8>;\n\t"
41+
"\n\t"
42+
"mov.u64 rd0, %0;\n\t"
43+
"mov.u64 rd1, %1;\n\t"
44+
"mov.u32 r20, %%tid.x;\n\t"
45+
"cvta.to.global.u64 rd2, rd0;\n\t"
46+
"mul.wide.u32 rd3, r20, 4;\n\t"
47+
"add.s64 rd2, rd2, rd3;\n\t"
48+
"ld.global.u32 r2, [rd2];\n\t"
49+
"\n\t"
50+
"mov.u32 r9, 33554432;\n\t"
51+
"mul.lo.u32 r0, r2, r9;\n\t"
52+
"and.b32 r1, 4278255360, r20;\n\t"
53+
"mad.lo.u32 r3, r0, 16, r1;\n\t"
54+
"sub.u32 r1, 620912, r3;\n\t"
55+
"mul.lo.u32 r19, 524288, r1;\n\t"
56+
"mul.lo.u32 r0, r19, r19;\n\t"
57+
"or.b32 r0, r0, 55244;\n\t"
58+
"\n\t"
59+
"exit:\n\t"
60+
"cvta.to.global.u64 rd4, rd1;\n\t"
61+
"mul.wide.u32 rd5, r20, 16;\n\t"
62+
"add.s64 rd4, rd4, rd5;\n\t"
63+
"st.global.u32 [rd4 + 0], r0;\n\t"
64+
"}\n"
65+
:
66+
: "l"(in), "l"(out), "r"(n), "r"(x)
67+
: "memory");
68+
}
69+
70+
static uint64_t fnv1a(const uint32_t* words, int n) {
71+
uint64_t h = 1469598103934665603ull;
72+
for (int i = 0; i < n; ++i) {
73+
uint32_t v = words[i];
74+
for (int b = 0; b < 4; ++b) {
75+
h ^= static_cast<unsigned char>(v >> (8 * b));
76+
h *= 1099511628211ull;
77+
}
78+
}
79+
return h;
80+
}
81+
82+
int main() {
83+
uint32_t h_in[kInputWords] = {};
84+
uint32_t h_out[kOutputWords];
85+
for (int i = 0; i < kOutputWords; ++i) {
86+
h_out[i] = kSentinel;
87+
}
88+
h_in[0] = kInput0;
89+
90+
uint32_t* d_in = nullptr;
91+
uint32_t* d_out = nullptr;
92+
check(cudaMalloc(&d_in, sizeof(h_in)), "cudaMalloc input");
93+
check(cudaMalloc(&d_out, sizeof(h_out)), "cudaMalloc output");
94+
check(cudaMemcpy(d_in, h_in, sizeof(h_in), cudaMemcpyHostToDevice), "cudaMemcpy input");
95+
check(cudaMemcpy(d_out, h_out, sizeof(h_out), cudaMemcpyHostToDevice), "cudaMemcpy output sentinel");
96+
97+
repro_kernel<<<1, kThreads>>>(d_in, d_out, kN, kX);
98+
check(cudaGetLastError(), "repro_kernel launch");
99+
check(cudaDeviceSynchronize(), "cudaDeviceSynchronize");
100+
check(cudaMemcpy(h_out, d_out, sizeof(h_out), cudaMemcpyDeviceToHost), "cudaMemcpy output");
101+
check(cudaFree(d_out), "cudaFree output");
102+
check(cudaFree(d_in), "cudaFree input");
103+
104+
std::printf("threads=%d n=%u x=0x%08x input0=0x%08x\n", kThreads, kN, kX, kInput0);
105+
bool any = false;
106+
for (int i = 0; i < kOutputWords; ++i) {
107+
if (h_out[i] != kSentinel) {
108+
any = true;
109+
std::printf("out[%d]=0x%08x\n", i, h_out[i]);
110+
}
111+
}
112+
if (!any) {
113+
std::printf("no output words changed\n");
114+
}
115+
std::printf("hash=0x%016llx\n", static_cast<unsigned long long>(fnv1a(h_out, kOutputWords)));
116+
return 0;
117+
}

0 commit comments

Comments
 (0)