Description
I am getting a miscompiled wasm that incorrectly performs arithmetic operations on u128
and Option<u128>
values. Based on my (poor) analysis of MIR, LLVM IR, and resulting wasm the issue seems to be on llvm side but it would be best for someone more competent to double-check that;
I managed to get the reproduction down to this code:
#[inline(never)]
fn get_total_opt(a: u32, b: u32) -> Option<u128> {
if a > 100 {
Some(a as u128 + b as u128)
} else {
None
}
}
#[inline(never)]
fn calculate(a: u32, b: u32, diff: u128) -> u128 {
let total_opt = get_total_opt(a, b);
// uncommenting any of these lines fixes the issue
// println!("{}", total_opt.is_some());
// assert!(total_opt.is_some());
total_opt.unwrap_or_default() + diff
}
fn main() {
let mut input_line = std::string::String::new();
std::io::stdin()
.read_line(&mut input_line)
.expect("Failed to read line");
let x: u32 = input_line.trim().parse().expect("Input not an integer");
let mut input_line = std::string::String::new();
std::io::stdin()
.read_line(&mut input_line)
.expect("Failed to read line");
let diff: u128 = input_line.trim().parse().expect("Input not an integer");
println!("{:?}", calculate(x, x, diff));
}
Compiled with
rustc wasmmain.rs --target wasm32-wasi -C "target-feature=+multivalue" -C opt-level=s
And launched with wasmtime
I expected to see this happen: when entering two values above 100 (i.e. 111 and 111) I expect to see correct result (i.e. 333)
Instead, this happened:
# wasmtime wasmmain.wasm
111
111
111
The behavior seems to only be exhibited in the calculate
function; replacing unwrap_or_default
with unwrap
or adding a side-effect between the call to get_total_opt
and the calculation seems to fix the issue. All of the changes to produced wasm are residing in the calculate
function (if we exclude offsets changes)
Also disabling the optimizations (opt-level=0
) or turning off the multivalue
feature both address the issue as well. The issue also happens on wasm32-unknown-unknown
target but I don't have as clean of a setup to showcase that
Emitted MIR
fn calculate(_1: u32, _2: u32, _3: u128) -> u128 {
debug a => _1;
debug b => _2;
debug diff => _3;
let mut _0: u128;
let _4: std::option::Option<u128>;
let mut _5: u128;
scope 1 {
debug total_opt => _4;
}
bb0: {
_4 = get_total_opt(move _1, move _2) -> [return: bb1, unwind unreachable];
}
bb1: {
StorageLive(_5);
_5 = Option::<u128>::unwrap_or_default(move _4) -> [return: bb2, unwind unreachable];
}
bb2: {
_0 = Add(move _5, _3);
StorageDead(_5);
return;
}
}
Emitted llvm-ir
; wasmmain::calculate
; Function Attrs: mustprogress nofree noinline norecurse nosync nounwind optsize willreturn memory(none)
define internal fastcc noundef i128 @_ZN8wasmmain9calculate17hd962bed4287d8d71E(i32 noundef %a, i32 noundef %b, i128 noundef %diff) unnamed_addr #5 {
start:
; call wasmmain::get_total_opt
%0 = tail call fastcc { i64, i128 } @_ZN8wasmmain13get_total_opt17h693d6b0c9dfa9b41E(i32 noundef %a, i32 noundef %b) #12
%total_opt.0 = extractvalue { i64, i128 } %0, 0
%total_opt.1 = extractvalue { i64, i128 } %0, 1
%1 = and i64 %total_opt.0, 4294967295
%switch.i = icmp eq i64 %1, 0
%spec.select.i = select i1 %switch.i, i128 0, i128 %total_opt.1
%_0 = add i128 %spec.select.i, %diff
ret i128 %_0
}
Emitted wasm (converted to wat with wasm2wat)
(func $_ZN8wasmmain9calculate17hd962bed4287d8d71E (type 9) (param i32 i32) (result i64 i64)
(local i64 i64 i64 i64)
local.get 0
local.get 1
call $_ZN8wasmmain13get_total_opt17h693d6b0c9dfa9b41E
local.set 4
local.set 3
local.set 2
local.get 3
i64.const 100
i64.add
local.tee 5
i64.const 100
local.get 2
i32.wrap_i64
local.tee 1
select
local.get 4
local.get 5
local.get 3
i64.lt_u
i64.extend_i32_u
i64.add
i64.const 0
local.get 1
select)
Both MIR and llvm-ir seem correct and with some wasm2c
and simplification it seems like the resulting wasm is unconditionally returning the p2
and then conditionally returning either p3
or p3
+corresponding u64
of the Option<u128>
and I was able to confirm that with this modified program
Modified code
Code:
#[inline(never)]
fn get_total_opt(a: u32, b: u32) -> Option<u128> {
if a > 100 {
Some(a as u128 + b as u128 + u64::MAX as u128)
} else {
None
}
}
#[inline(never)]
fn calculate(a: u32, b: u32, diff: u128) -> u128 {
let total_opt = get_total_opt(a, b);
// uncommenting any of these lines fixes the issue
// println!("{}", total_opt.is_some());
// assert!(total_opt.is_some());
total_opt.unwrap_or_default() + diff
}
fn main() {
let mut input_line = std::string::String::new();
std::io::stdin()
.read_line(&mut input_line)
.expect("Failed to read line");
let x: u32 = input_line.trim().parse().expect("Input not an integer");
let mut input_line = std::string::String::new();
std::io::stdin()
.read_line(&mut input_line)
.expect("Failed to read line");
let diff: u128 = input_line.trim().parse().expect("Input not an integer");
println!("{:?}", calculate(x, x, diff + u64::MAX as u128));
println!("{:?}", x as u128 + x as u128 + diff + u64::MAX as u128 + u64::MAX as u128);
}
Output:
111
111
36893488147419103342
36893488147419103563
Meta
rustc --version --verbose
:
rustc 1.78.0 (9b00956e5 2024-04-29)
binary: rustc
commit-hash: 9b00956e56009bab2aa15d7bff10916599e3d6d6
commit-date: 2024-04-29
host: x86_64-unknown-linux-gnu
release: 1.78.0
LLVM version: 18.1.2
rustc +nightly --version --verbose
:
rustc 1.81.0-nightly (aa1d4f682 2024-07-03)
binary: rustc
commit-hash: aa1d4f6826de006b02fed31a718ce4f674203721
commit-date: 2024-07-03
host: x86_64-unknown-linux-gnu
release: 1.81.0-nightly
LLVM version: 18.1.7