-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Possible bug in WASM: Pointer math issue when using Emscripten as backend for FreeBasic #20325
Comments
My emcc version is: emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.46 (1960782) |
Looks like undefined behavior: $ ./emcc a.cpp -fsanitize=undefined
$ nodejs c.out.js
a.cpp:4:33: runtime error: left shift of negative value -1 |
I tried to modify the test code to this:
Same issue, "index out of bounds".
works normally. |
Even weirder: if compiled with the parameter "-fsanitize=undefined", it returns no error and works as expected. Without that parameter, it crashes |
Further information: if the parameter "-sWASM=0" is added, all seems to work as expected. Even the first code (with the syntax that is supposed to produce undefined behavior) works perfectly. The problem happens only when WASM code is produced, not when JavaScript code is produced |
All the code examples above also crash when compiled with gcc or clang:
What makes you think this is an emscripten specific issue? |
Have you compiled it with no change to native code for your machine? You should either try it on a 32 bit machine, or modify the line:
To: The pointer is temporarily cast to an integer, and Emscripten uses 32 bit pointers, while your machine likely uses 64 bit pointers |
I see, yes with I also can't repro with emscripten:
|
I confirm: with -O2 it doesn't happen. Even weirder |
Also, I read that the left shift of a negative value is defined in C++20 specifications, actually |
does not seem correct? So If you run with
which is like expected, since the memory access does go out of bounds of the array.
The reason for the issue hiding with optimizations is that it all gets optimized out since |
Oh, looks like the
|
I was able to get
|
Then, why does it work if I disable the WASM and use the javascript emitter? It should either fail in both cases, or work in both cases? And why does it work if the array is declared inside "main()"? Also, there is a double casting: I mean, let's say we declare a pointer p int arr[100]; //an array. Let's suppose it's allocated at address 1000 |
Also: why this does cause an exception:
And this doesn't?
Isn't the code perfectly equivalent? I just replaced "i" with "1" inside the formula |
Clearly the compiler/optimizer in llvm is doing something different in those two cases. My guess is that this is an issue the relates to offset parameter of the wasm (See https://github.com/sunfishcode/wasm-reference-manual/blob/master/WebAssembly.md#store) |
It seems that its not the
The store instruction which is the one that is trapping looks reasonable. We have |
Ops, sorry, you are correct. I see now, the many parentheses tripped me up and I interpreted the subtraction before the second cast. Indeed it does look like something is off here. A smaller test case int arr[2];
int main(){
for (int i = 0; i < 2; ++i) {
*(int*)((int)arr + 4 + -i) = 1;
}
} also fails, whereas int arr[2];
int main(){
for (int i = 0; i < 2; ++i) {
*(int*)((int)arr + 4 -i) = 1;
}
} doesn't. So something looks fishy here. |
I can confirm that. So, the issue seems to be at WASM level |
Looks like -fsanitize=undefined also masks the issue. int arr[2];
int main(){
for (int i = 0; i < 2; ++i)
*(int*)((int)arr + 4 + -(i<<2)) = 1;
} passes with |
Simpler test case without a for loop: int arr[2];
int i = 4;
int main() {
*(int*)((int)arr + 4 + -i) = 1;
} |
Oh nice, now the failing main function is just a few lines since its doesn't do |
Which browser are you using? I am using Firefox |
You can repro in node on the command line, no need for a browser:
|
I see. I just want to rule out any possible bug in the virtual machine itself |
Failing wasm: ( (func $__original_main (result i32)
(local $0 i32)
(local $1 i32)
(local $2 i32)
(local $3 i32)
(local $4 i32)
(local $5 i32)
(local.set $0 (i32.const 0) )
(local.set $1 (i32.load offset=65536 (local.get $0) ) )
(local.set $2 (i32.const 0) )
(local.set $3 (i32.sub (local.get $2) (local.get $1) ) )
(local.set $4 (i32.const 1) )
(i32.store offset=65544 (local.get $3) (local.get $4) )
(local.set $5 (i32.const 0) )
(return (local.get $5) )
) Passing wasm: ( (func $__original_main (result i32)
(local $0 i32)
(local $1 i32)
(local $2 i32)
(local $3 i32)
(local $4 i32)
(local $5 i32)
(local $6 i32)
(local $7 i32)
(local.set $0 (i32.const 0) )
(local.set $1 (i32.load offset=65536 (local.get $0) ) )
(local.set $2 (i32.const 65540) )
(local.set $3 (i32.const 4) )
(local.set $4 (i32.add (local.get $2) (local.get $3) ) )
(local.set $5 (i32.sub (local.get $4) (local.get $1) ) )
(local.set $6 (i32.const 1) )
(i32.store (local.get $5) (local.get $6) )
(local.set $7 (i32.const 0) )
(return (local.get $7) )
) |
How do you get the nice one-liner wasm disassembly @sbc100 ? wasm-dis gives that verbose output that's not that nice to diff. |
If you compile test.c to test.o you can then link with -O2 to get a much smaller failing
This gives:
|
To get linear dis-assembly I use |
Hmm, for me it doesn't fail with -O1 or higher. |
The first code ends up doing a memory write to memory address |
You compile test.c with
|
(note that I'm linking test.o .. compiled with -O0) |
I think the problem is that |
The failure does occur equally in
(tested on macOS Catalina 10.15.7 (19H2)) So the behavior is either specced to work like that, or every VM got it wrong (seems unlikely).
Yeah, the following code does give out of bounds: int main() { ((char*)-1)[13] = 1; } whereas int main() { ((char*)0)[12] = 1; } doesn't raise an out of bounds error. Disassembly for the failing case being (func $__original_main (result i32)
(local $0 i32)
(local $1 i32)
(local $2 i32)
(local.set $0 (i32.const -1) )
(local.set $1 (i32.const 1) )
(i32.store8 offset=13 (local.get $0) (local.get $1) ) // RuntimeError: memory access out of bounds
(local.set $2 (i32.const 0) )
(return (local.get $2) )
)
Wouldn't that need a combination of i32->u32->u64 32-bit->64-bit widening to happen? i.e. even if address was interpreted as u32, 0xFFFFFFFF, then adding 13 to it would still produce 12 as result? Only if it was interpreted as u32, then internally extended to u64 for the offset calculation (to generate 0x10000000C) would that be a problem? Though I suppose since it generates a x86 mov instruction, the CPU will always treat both the base number and offset as u64. |
Even further simplified test case: char arr[2];
char i = 1;
int main() {
*(arr + 1 + -i) = 1;
} which is quite reasonable code. Although for some reason, it generates very silly wasm code: (func $__original_main (result i32)
(local $0 i32)
(local $1 i32)
(local $2 i32)
(local $3 i32)
(local $4 i32)
(local $5 i32)
(local $6 i32)
(local $7 i32)
(local $8 i32)
(local.set $0 (i32.const 0) )
(local.set $1 (i32.load8_u offset=65536 (local.get $0) ) )
(local.set $2 (i32.const 24) )
(local.set $3 (i32.shl (local.get $1) (local.get $2) ) )
(local.set $4 (i32.shr_s (local.get $3) (local.get $2) ) )
(local.set $5 (i32.const 0) )
(local.set $6 (i32.sub (local.get $5) (local.get $4) ) )
(local.set $7 (i32.const 1) )
(i32.store8 offset=65541 (local.get $6) (local.get $7) )
(local.set $8 (i32.const 0) )
(return (local.get $8) )
) the (local.set $1 (i32.load8_u offset=65536 (local.get $0) ) )
(local.set $2 (i32.const 24) )
(local.set $3 (i32.shl (local.get $1) (local.get $2) ) )
(local.set $4 (i32.shr_s (local.get $3) (local.get $2) ) ) part could be replaced by a (local.set $4 (i32.load8_s offset=65536 (local.get $0) ) ) seems that Wasm backend does not understand how to emit |
Looks like char arr[2];
int i = 1;
int main() {
*(arr + 1 + -i) = 1;
} generates the simplest test case: (func $__original_main (result i32)
(local $0 i32)
(local $1 i32)
(local $2 i32)
(local $3 i32)
(local $4 i32)
(local $5 i32)
(local.set $0 (i32.const 0) ) // $0 = 0
(local.set $1 (i32.load offset=65536 (local.get $0) ) ) // $1 = MEM8[65536] (== 1)
(local.set $2 (i32.const 0) ) // $2 = 0
(local.set $3 (i32.sub (local.get $2) (local.get $1) ) ) // $3 = $2 - $1 = 0 - 1 = -1
(local.set $4 (i32.const 1) ) // $4 = 1
(i32.store8 offset=65541 (local.get $3) (local.get $4) ) // MEM8[$3 + 65541] = $4; RuntimeError: memory access out of bounds
(local.set $5 (i32.const 0) )
(return (local.get $5) )
) |
On that last testcase, I don't know if it has undefined behavior in C, but looks like it crashes in Looking at that $ wasm-opt a.out.wasm --simplify-locals --coalesce-locals --reorder-locals --vacuum --extract-function=__original_main --print
extracting __original_main
(module $a.out.wasm
(type $0 (func (result i32)))
(memory $0 256 256)
(data $.data (i32.const 65536) "\01\00\00\00")
(export "__original_main" (func $__original_main))
(func $__original_main (result i32)
(i32.store8 offset=65541
(i32.sub
(i32.const 0)
(i32.load offset=65536
(i32.const 0)
)
)
(i32.const 1)
)
(return
(i32.const 0)
)
)
) That code is expected to crash: the load returns 1, and then 0 - 1 is negative 1, and that is a large unsigned value. Adding the offset of That is, |
Are you referring to the test code at char arr[2];
int i = 1;
int main() {
*(arr + 1 + -i) = 1;
} I am fairly positive that this should not have any undefined behavior. The only thing that comes to mind in the standard is that weird rule that pointer calculations should not exceed [begin,end] ranges (even if not dereferenced) - but here Looks like the following test case also generates an error: char arr[2];
int i = -1;
int main() {
*(arr + 1 + i) = 1;
} (one can also remove the +1 there and still observe a crash, and while the resulting address should line up to a known ok value, that would be UDB C/C++ standards-wise) For all intents and purposes, that application should be the same as char arr[2];
int i = -1;
int main() {
char *ptr = arr + 1 + i;
*ptr = 1;
} (which does not crash) I can't think of a reason that there should be UDB in either of these programs. Let's invoke some more people to poll: @tlively @dschuff ? This scenario seems like an incorrect reversal of a
Although not sure how this could be fixed without pessimizing codegen. To me seems like load/store offsets would want to work with u64 sizes to take advantage of overflow(?) |
Sorry, I should have said, I referred to the last testcase before my last comment. |
I agree that wasm disassembly is expected to crash given the wasm semantics. It feels like the root issue would be that wasm code should not have gotten generated in the first place? |
I agree, if there is no UB here then that looks like a wasm backend bug. I'm hoping one of the people you cc'd can confirm if there is or isn't UB here. |
Stumbled today on a video by Jonathan Müller (@foonathan), who is part of the standardization committee, and gave a presentation at C++ On Sea 2023 conference this June: https://youtu.be/zGWj7Qo_POY?t=34 there he does mention that |
I have found some weird issues that lead to code producing the exception "index out of bounds". The minimal code that produces the error is:
This code, that is very similar, seems to work with no problem:
What might cause it?
In case you wonder why I am using such a weird code, instead of a simple "arr[13-i]=i;", the reason is that the code is produced by the C emitter of the FreeBasic compiler (and it works perfectly with the GCC compiler on other platforms). More information is available on the FreeBasic forum, in this thread
The text was updated successfully, but these errors were encountered: