It Is Never a Compiler Bug Until It Is
r6.ca
r6.ca
It kind of looked like it was being fed nonsense speed values, so I got the GPS log from my manager and checked - but no weird speed values, actually a remarkably clean GPS log. Replayed his GPS on my phone - worked perfectly fine, buttery smooth. Eventually it came out that it only happened on my manager's phone. Borrowing said phone and narrowing things down with printf, I showed that my core animation function was being called with the correct values (a, b, c, d) but was being run with the wrong ones (a, a, c d). This is when my manager thought to mention that he was running the latest internal alpha preview of Android.
Searching Android's bug tracker for JIT bugs, I found that they had a known register aliasing bug. Honestly I have no idea how it ran well enough to get to my code in the first place. But I tagged my weird animation bug as related to that (they didn't really believe me) and ignored it until they fixed their thing, at which point it went away.
I was immediately sure it was a compiler bug when I realized I could make the code work correctly if I changed the order of variable declarations. It would happen on something like:
int a; int b; float c;
(again, that's only illustrative - too long ago for me to remember the specifics)
But not with:
int a; float c; int b;
Folks in my team didn't want to believe it at first :)
Edit: found it here - https://github.com/dotnet/runtime/issues/17395
Not quite as I described but close.
This is an example of "metamorphic testing".
Sometimes someone would actually try to solve these, but I preferred the times where someone just changed the Proguard config and hope it shuffled things around enough that it didn't trigger whatever bug the JIT had.
Quasi-related question: app installs/updates have been very very consistently SIGBUSing system_server with BUS_ADRALN on my ancient Galaxy Note 3 for about a year, but downgrading the Play Store app to the factory version (5.5.12 :D) makes the problem go away completely.
I've tentatively considered writing something up for issuetracker.google.com, but besides tons of logcats I'm not sure what info to provide, and I also wonder if the info would be considered useful due to the age of the device. Any advice on whether/how to proceed would be appreciated!
A bug report would be the first thing I'd want for something like this - in addition to logcat info it includes a bunch of process information, and apps can add custom data to the report. It'd also be nice to know anything to narrow down when the problem started - whatever bounds you can provide on when you were last absolutely certain it was up to date and working, plus your best guess. Someone's going to have to dig a Galaxy Note 3 out of a drawer somewhere, go into the archives of old Play Store versions, and try installing them until they find the version that broke it, so anything that makes that binary search process less terrible would be a start.
> there are very few people assigned to maintaining old systems, and it's quite hard to get these issues brought to anyone's attention. The ones I mentioned only got fixed because they crept up to the top most common causes of crashes.
Thanks for this info. It kinda matched my own intuition about what to expect, but I wasn't sure where/how I might find out for sure. It's nice to be able to calibrate how to optimize effort.
> A bug report would be the first thing I'd want for something like this - in addition to logcat info it includes a bunch of process information, and apps can add custom data to the report.
Ah, of course. That's straightforward to do; I can just watch logcat for system_server crashes then automatically take bug reports when they occur.
> It'd also be nice to know anything to narrow down when the problem started - whatever bounds you can provide on when you were last absolutely certain it was up to date and working, plus your best guess.
IIRC™, this has been happening from day 1 when I was given this phone (it was previously sitting unused in a drawer, yay).
> Someone's going to have to dig a Galaxy Note 3 out of a drawer somewhere, go into the archives of old Play Store versions, and try installing them until they find the version that broke it, so anything that makes that binary search process less terrible would be a start.
Oh meep. Of course.
I'd have no problem loaning the device (perhaps the carrier-specific image it's using is implicated), but that's probably a huge pile of overhead to deal with. I wonder if I can help out with the bisection process myself?
Hmm. Given that this is the Play Store and the device is (currently) not rooted, I couldn't do it the "russian" way with random APK sites even if I wanted to.
I'm very curious what the internal path is here - does it depend on rooting or is there a way to isolate devices and send them specific versions of apps?
(Now I'm imagining being given a (bespoke) HTTP endpoint to hit (just for this) to switch versions...)
Maybe the best path forward would just be to root this thing already. I'd be very surprised if rooting affected the situation.
Thanks very much for the thought about bisecting GPS though. Definitely hadn't thought of that myself at this point, and don't think I would have anytime soon.
Good luck with your quest though!
It probably is, but there's also the
Failure [INSTALL_FAILED_ALREADY_EXISTS]
brick wall which makes perfect sense, is not especially specific to the Play Store, and will likely require root (then some careful `mv`s) to squish. Yayyy.> Good luck with your quest though!
Much appreciated :) thanks again for the insight/feedback!
https://www.youtube.com/results?search_query=Mozilla+Hacks+C...
We use LuaJIT pretty extensively and some codepaths trigger a crash-on-assert which we've confirmed is a JIT bug. But not consistently, the weather has to be just right. 95% of the time, randomly inserting `assert(true)` into the code un-breaks it.
We're using the beta, so this is an expected sort of thing. I'd love to be the guy who squashes it, but that's way outside my wheelhouse. I'm left crossing my fingers and hoping one of the updates makes it go away.
Globally disabling the JIT is a nice way to confirm that using LuaJIT was a good idea, though. Instant code starts taking seconds to complete.
In the end I whittled it down to the following tiny C# program:
namespace UhOh
{
internal class Program
{
private static void Main()
{
System.Console.WriteLine(Test(0, 0));
}
private static bool Test(uint a, uint b)
{
var b_gte_a = b >= a;
var b_gt_a = b > a;
System.Console.WriteLine(b_gte_a);
return b_gte_a && b_gt_a;
}
}
}
Compiling and running this with Microsoft's .NET stack with versions 4.7.0 and below, the output was incorrectly: "True, True" instead of "True, False". (IIRC, it also had to be a 64-bit Release build.)The intermediate language was correct; it was a bug in RyuJIT.
At the time we were working on a new "feature phone" with a 160x120 pixel display in 4 shades of grey, which was a huge upgrade compared to our previous models. Another feature was full screen images for the various applications, and we'd been implementing is into the software and testing it for weeks without problem. After the development cycle came to an end, our release team created a new software release and sent it to our test department, which almost instantly reported graphical errors back to us. We tested the software image on our own handsets, and half the screen was "garbage".
We spent weeks looking over the merged code, as well as debugging pointers code and found nothing. It wasn't until we were stepping through the paint code with a Lauterbach debugger that we noticed something was "off" with a pointer.
The platform was a 16 bit platform and memory was addressed using a page pointer pointing to a 64kb memory page. When we traversed the bitmaps fate would have it that this particular bitmap, in this particular build, was split between two pages, and when incrementing a pointer beyond the page limit, instead of incrementing the page pointer, it simply just overflowed and started from the beginning of the current page.
Another interesting bug we chased in the compiler was it's inability to add more than 2 variables in initial assignment.
i.e.
int a = 1+2; // a = 3
int b = 1+2+3; // b = 3
int c = 10 + a + b; // c = 13
That took a while to figure out.Did they write a special expression parser just for declarations? I mean, this shouldn't be possible if there was just one expression parser?
One day I found out about DJGPP, and even if the download cost a fortune in phone time, life was so much better.
Now the C reference book included with MIX C turned out to be much better than the compiler itself, and served me well for a decade.
I was working on a defense contract, on a government system, where I was constrained by local IA policy to specific versions of various tools, including a relatively ancient version of gcc.
I can't recall exactly what the problem was, but I do remember figuring out after doing some research that the bug that was biting me had been identified and fixed in a later version of gcc. Which I was not allowed to install. So I had to implement a hack-tastic workaround anyway.
One of the best parts about that job - I was integrating the tool I was writing (in Python with Tk - it was the only installed and approved GUI library I could use) with a really old signal analysis library that had originally been written for VMS back in the day - then ported to SPARC/Solaris - then ported again to x86 (yes, VMS heritage was evident in places). Through many years of flowing through many different maintenance contractors, the library had become a giant amalgamation of Ye Olde FORTRAN, C, C++, and Python. To build it I needed a specific version of the Intel FORTRAN compiler, which my employer would not purchase, and the client IA policy would not allow on their system anyway. With much hackery, I managed to coax the damn thing into building using the "approved" gfortran that was already on the network.
Egad, what a horrible job that was.
In about 1988, a bug in Apple's MPW Pascal compiler. I refused to believe it was a compiler bug, until I finally inspected the generated code. IMO the only way to be taken seriously about a compiler bug is to distill the defective compiler behavior down to a short (like one page) example. Also helpful is to show the generated code and how it is wrong. Bug was acknowledged and fixed.
In the mid 1990s, dabbling with C++ on (classic) Mac, I upgraded to a (name brand withheld) version 8.0 C++ compiler. The generated code behavior was obviously wrong. To make matters worse, it was possible to crash the compiler. The number of problems with that compiler were so bad that I simply ditched that compiler, and it didn't last much longer commercially. Sad, because its predecessor compilers (mostly C and Pascal) had been very good.
I've heard of story where government standard explicitly demanded a lower key size of encryption, way below current industry standard, for 'secure' applications.
I often think that we create inferior solutions because it’s too hard to get newer methods approved. But then the newer methods may also cause problems.
Windows (including CE) has a crash detection system called "structured exception handling". You can use this to route segfaults to either "try/catch" (don't) or "__try/__except" special handlers. We had one of these to log the error and show a crash screen. It worked fine on the desktop. It worked fine on the MIPS systems. On ARM systems, it sometimes didn't work. At these times, the debugger didn't work properly either.
I eventually found the kernel stack unwind code (with WinCE you get some, but not all, of the kernel source). For some reason this was looking at the jump instructions on the way down, and the particular problem was virtual method calls were implemented as "LDA pc, [r3]" after computing the target vtable location in r3.
r3 is in the "clobberable" set of registers in the ARM calling convention, so if it got overwritten lower down the call stack the unwind handler would read the wrong value and fail to follow the stack.
Fortunately it turns out there were two different versions of the ARM compiler, shipped in different toolkits by Microsoft (why? who knows) and using the other one didn't trigger the bug.
We checked the known-good compiler into source control and kept it there as critical build infrastructure.
I wonder what percentage of the job market that niche will be in fifty years.
you're so lucky. between just 2014 and 2018 I reported something like 30 bugs to msvc, gcc, clang (in decreasing order)
This is why preferably everyone should learn to read assembler output. This is not limited to C/C++/Rust/etc. native code, the same output is typically also available for example for JVM and Javascript JIT.
Haven't found any miscompilations so far (unless you count braindead codegen), but quite a few hardware bugs. Including one CPU bug.
First of all, https://godbolt.org/. C/C++/Rust to assembler, very useful.
Learn the calling conventions, basic arithmetic, flags, conditional branching. Understand stack management.
Single step through functions in a debugger to see how things work, and for example how a stack frame is set up. Pay attention how registers and flags are affected by the execution. See how conditional branches are affected by the flags.
When you're looking at the code, remember that there are often weird looking details, like unused portions in stack and in compiled function codegen (loops, entry points) for alignment purposes — modern CPUs hate unaligned things.
Note: Some x86 instructions have implicit register use that might not be directly obvious. Like PUSH, POP, IMUL, IDIV, LOOP, STOS[BWD], MOVS[BWD], etc. They can affect registers that are not mentioned in the instruction operands.
In general, if the things look weird, just google the instruction. Much less surprises in other mainstream architectures, like ARM. All architectures do have vector instructions that might confuse you at first, like x86 vectored double precision add, VADDPD. Again, just web search them. No one remembers all of the instructions by heart, there's just no point.
Web search for assembler tutorials and simulators. They're too many to list, just pick something suitable for your taste.
In short, play around. Don't get scared by something weird, just look it up.
Don't stress if you don't understand everything, you can always look it up or try it out in a debugger. Even a little bit can help quite a bit.
JITs usually have some way to display generated assembler. For example, to see the native x86/ARM/whatever code generated by JVM you'd say something like:
java -Xbatch -XX:-TieredCompilation -XX:+PrintCompilation
Maybe throw in -Xcomp for maximum optimization. I don't remember the details, just look them up. :-)Other JITs have similar ways, once you know this kind of thing exists it should be easy to look it up.
Another good tip: download the official architecture manuals, they're freely available for the most relevant major architectures (and for x86, both Intel and AMD have their own version). They're monstrous doorstoppers (several volumes with many thousand pages), but include in excruciating detail the description of every single instruction, with the advantage that they'll work even if your Internet connection is offline.
well, at least the official ones with the officially supported operands. Sandsifter has a lot to say about how many undocumented instructions exist on just the "exposed" x86 part of the CPU, nevermind the ARM and other sub ring-0 stuff.
When you write assembly code, it's generally specific to several details, which in turn affects your choice of tutorials from which to work:
1) The target architecture (x86-64, AArch64, MIPS, etc.) This defines what instructions you have available, the memory model, etc.
2) The architecture ABI. E.g., what's the protocol for passing parameters to functions you call, and for receiving the return code. [0]
3) The particular assembler you plan to use. Gnu As (and I assume others) provide some directives that don't map directly to machine instructions, but do some book-keeping to make your life easier.
4) (Potentially) the file format for the resulting code [1]. I think this is one area where the assembler and linker utilities can shelter you from the ugly details.
FWIW regarding JIT: Lately I've started playing around with Xbyak [2]. It's probably a bit light on the documentation, but for the most part I've found it to be an easy way to get started with JIT.
[0] https://en.wikipedia.org/wiki/X86_calling_conventions#System...
[1] https://en.wikipedia.org/wiki/Comparison_of_executable_file_...
Maybe start with JF Bastien talk at CppCon 2020. He did a very nice historical overview of JITs.
"Just-in-Time Compilation"
https://www.youtube.com/watch?v=tWvaSkgVPpA
Then some quick stuff:
"Understanding HotSpot JVM Performance with JITWatch"
https://www.infoq.com/presentations/jitwatch/
In Visual Studio you can directly see CLR JIT Assembly in debug mode (F12)or via WinDbg and SOS plugin.
https://docs.microsoft.com/en-us/windows-hardware/drivers/de...
https://docs.microsoft.com/en-us/dotnet/framework/tools/sos-...
You can also play with it online, https://sharplab.io/
V8 Blog
With GCC, its as simple as adding `-save-temps=obj`. You get preprocessed source and assembly emitted alongside the object files.
For example:
- With Clang, you can dump the C/C++ AST and/or the LLVM IR.
- With GCC, you can dump the Gnu Assembly (with source-level annotations).
These views can be helpful, especially for someone unfamiliar with the target machine's instruction architecture and ABI.
Few hours later, he verified and produced on-list a reproduction case where a variable could not be incremented by 1 but could by 2 or any other number.
Turns out he’d been taught in typing class that l (lowercase L) could be used for 1 and carried that into computing.
WONT_FIX
A good way to keep them busy would have been to demand they type an exclamation point.
(Hint: Backspace-and-overstrike a period with a single quote.)
[0] https://play.rust-lang.org/?version=stable&mode=release&edit...
The comparison is deterministic - the perhaps unexpected part is that two distinct but identical functions in the source code are folded into one in the binary.
Through there are bugs involved, too ;=)
(Due to const folding comparisons vs. doing them at runtime there was/is non-determinism in the same build between different call sites ... )
I once ran into a bug where "bash" would run commands out of order. It wasn't hard to trigger the bug, but it wasn't deterministic either.
When I first noticed the bug on my production systems it drove me insane, since the logs being generated were impossible. It took me a weeks to figure out that bash was running commands out of order.
Then, when I tried to report this bug, I ran into a lot of resistance. First over IRC, nobody believed this could possibly be happening -- and I was eventually directed to the mailing list [0], where the maintainers were initially not able to replicate it, but eventually more required elements were identified and the bug was fixed.
[0] https://lists.gnu.org/archive/html/bug-bash/2015-06/msg00010...
This was a 16-Bit processor and the C-compiler had a "funny" bug. If you had a struct with 3 8Bit Values in a row and a 16Bit Value afterwards it would overlap the 8Bit Value with the 16Bit value:
struct {
int8 a;
int8 b;
int8 c;
int16 d;
}
In this case the variable c and d would have the same address. This was on a cpu where we didn't had a debuger (not enough memory left for it), we only had a serial port for debuging.I do this routinely where the target has 256GB of RAM, and (not incidentally) specialized network hardware, but no dev infrastructure except gdb-server (which provides the stub) and sshd. I build in a docker image that matches the target, but with dev tools, with the output bin directory sshfs-mapped to a directory on the target. I run the binary on the target under gdb-server, opening a socket listener. Then I run gdb natively on my dev machine, and `target remote server:61231` to attach to that socket. If I didn't have easy access to listening ports on it, I could ssh-tunnel one in.
So, a serial port and small RAM doesn't have to mean you have no debugger.
In our case it would probably not have helped. We had a fixed old mcu board where the functionality grow over the years. We were fighting over bytes...
I assume, by saying there wasn't room for debugging functionality, the poster meant that the "jtag" or equivalent hardware port simply couldn't work for single stepping due to the particular architecture requiring compiled-in cooperation of the firmware, and they didn't have the kilobytes of memory to spare.
These days, it's becoming more reasonable to throw Linux based compute nodes at problems previously best served by microcontrollers. A more powerful CPU isn't a superset of a microcontroller, though. Microcontrollers are still necessary when you have "hard" timing requirements and you need to account for where your CPU cycles are going. Even seemingly "solved" problems like participating on a CAN bus is difficult for a Linux based node. For example, while you can easily purchase CAN interface boards for raspberry pi and send and receive messages, you are pretty much guaranteed to drop some percentage of incoming messages at realistic bitrates. All the boards use MCP2515 SPI CAN controllers, and the linux driver simply can't schedule SPI transfers in response to interrupts fast enough to avoid mailbox overruns inside the controller. Maybe it's somehow been cleverly fixed since I last looked at it though?
Routing gdb stub traffic through a hosted Linux to an RTOS task or coprocessor is not an elementary exercise, but is something an engineering student might be expected to implement, even as just part of the real project.
I think that made me just a little paranoid. I generally trust things, but depending on their popularity and likely it is my code path is run by lots of users, I realize library (and compiler!) bugs happen.
Back in the '80s, the C++ compiler was `cfront`. We spent half of every day bisecting source files to identify the line that would crash the compiler, and doctor it to step around the bug.
People who used to use the Lucid compilers said they were happy when Lucid flopped, because from then on their compiler only had known bugs, instead of a new crop every few months.
Things are better, nowadays, with compilers.
I discovered a bug in the Swift standard lib once, and it took ages before I got to the point where I decided to strip out my own code, just to make sure it was me. And it wasn't, there was genuinely something wrong in the lib that other people on SO were also able to reproduce.
Good on him for finding a bug in secp256 too. When it comes to cryptography code, it can be very hard to know what the right answer is. I always find some examples on the internet and put them in a unit test to make sure I'm not misusing the API, because if you do your answer looks the same: bunch of numbers in a byte array. To know that your numbers are wrong, you need to be sure you are testing them correctly. Which you can't be if you don't know if you're using the API correctly.
1) your test suite works over several revisions of the compiler
2) after you upgrade, suddenly one test fails (especially if it's out of hundreds)
2a) you can isolate the minimum condition and it really leaves your head scratching
3) rewriting the code in a slightly different way makes it pass
The time I found a compiler bug it was because of an optimization that missed a corner case that I just happened to be using.
Broken: table[table_i].values[j] && j<category_map_count && to_i<category_array_len; Fixed: j<category_map_count && table[table_i].values[j] && to_i<category_array_len;
Optimizer: "j was used to access values array, j outside array would be undefined behavior, therefore it must have been within the array boundaries and does not need to be tested."
Oh, and threading bugs where some variable that should have been mutex locked or made atomic gets moved in or out of a register depending on the phase of the moon, causing other threads to fail to notice changes to that variable...but add a debug line and it suddenly works.
int foo(struct data *data) {
struct member *member = &data->member;
if (data == NULL) {
return -EINVAL;
}
// Do stuff
}
Assigning a value to member is just a matter of taking data and adding the value of member, so won't explode if data is NULL since it's not actually dereferencing that address. But this is still in undefined behaviour territory, so gcc assumed that we must know that data could never be NULL and optimised out the check.This sort of thing is why I have definite feelings about the use of C in security sensitive contexts.
Well, until you start suspecting hardware bugs.
Me: I have memory corruption when I call your API. IBM: trust us, our API DLL is perfectly compatible with your old Windows 32 bit client program! We changed nothing! Me: I have stack overruns. 4 bytes of return value from you overwrite 4 bytes of variables, whatever I declare last in my function. IBM: look at the source of our API façade! It's unchanged! (it was, except for harmless additions). Me: your compiled code is fairly similar, but the return value is bigger. (At this point, I was already on very friendly terms with Ghidra and with the Visual Studio remote debugger.) IBM: we just recompiled our code!
But they recompiled it with a newer compiler: time_t had changed from 32 to 64 bits, changing the size of the returned unions in their DLL but not in my client.
This hits home.
Anyway, my experience is that compiler bugs do exist, but maybe 99% or so of "compiler bugs" reported by my users turn out to be undefined behaviour in their code.
If you're working with a new language or quickly changing, e.g. Nim, Crystal, etc, or even something as old as Rust, then it can much more easily just be a compiler bug...
The buggiest compiler I ever used was a C compiler that ran on a PC and generated code for the 68000 processor. We seemed to trip over something about once a month.
Sometimes, for no reason, GCCE just crashes compiling totally innocent code. Usually, a minor rewrite of the logic helps, or even weird edits such as adding a new (useless) parameter to a method.
The last GCCE toolchain for Symbian was released by CodeSourcery in March 2012. It contains GCC version 4.6.3. It is theoretically possible to adapt and compile a newer version, but the sources need so many edits that I gave up after a few days.
It had 60% of phone market share and it was RISING despite the launch of iPhone and Android.
The reason is that:
1. It worked great.
2. Brazil had a vibrant dev community (people would even port PC games to Symbian O.o)
3. It was much cheaper than iPhone and clones.
4. Nokia phones were just solid and awesome.
After MS made that memo that killed Symbian, it died almost instantly, people got so disappointed with MS that they started to switch to android, even if it was some chinese-made "shit-phone" instead of "feature-phone", the amount of really, really crappy androids that flooded the market was mind boggling, many didn't even work right, for example wouldn't complete calls properly or wouldn't connect to some Wi-Fi channels.Took a long time to find, because there were no errors, just wrong results (a specific if statement taking the wrong branch).
Trying to get assistance from others were mostly met with responses along the lines of "It's probably a race condition" (in single-threaded code) / "very unlikely to be a bug in the JIT". I did end up finding a way to disable JIT for the specific method, which solved the issue, and never got around to finding the root cause. I do believe it has been fixed in the meantime at least.
I haven't run into major compiler bugs since then, but often have to dive deep into libraries to find obscure bugs (database drivers and web servers most often).
Used to be, a compiler bug was right up there with a memory issue in your list of 'what might be wrong'.
The gist is that our security issues could come layers away from where we may expect them to, all the way up to the compiler. It's a great paper, but who would have expected anything less from Ken Thompson.
The story is that I checked in a code change that passed unit tests locally, that then broke an automated build. This was bizarre because this was at Google and our check-in process guaranteed that we had run the test suites successfully, which required the very compile that broke.
It turns out that the local compile was with GCC, and the automated one was Clang. The construct that they treated differently went like this.
There was a class A with a protected property bar, and a subclass B. I also had unit test code in a class TestB which was a friend class to B. And in that unit test code I accessed foo.bar where foo was of class B.
GCC looked at that access, decided that bar is protected, foo is of class B, and TestB is a friend, so TestB has access.
Clang looked at that access, found that bar is protected and from class A, TestB is NOT a friend, so TestB had no access.
The problem went to a local expert who read the spec and decided that GCC was per the spec, Clang was not, and submitted a bug report to Clang.
As for me I figured that if the very first thing that I thought to try with protected, friend and subclasses was an edge case that nobody could agree on, perhaps C++ wasn't the right language for me...
It took several months to figure out what was happening even though in retrospect it should have been pretty obvious.
But it was definitely a compiler bug either way. There was a POP (or maybe it was an UNLK) instruction, followed by a dereference of the PREVIOUS value of SP. That was bound to fail on any architecture that used the current process stack to handle interrupts. It just so happened that most of the development of both the compiler and the application code was being done on unix machines, which happens not to be such an architecture (interrupts are handled on the kernel stack) and so the bug never manifested itself there. But such architectures were common then, and are still common in embedded systems today.
It was a bug in the runtime.
JDK 1.0.3 came out, and ran that code just fine.
The hits in the implementation of draft-brezak-win2k-krb-rc4-hmac-04.txt seem ... interesting.
I've also had a library + debugger bug that really left me pulling my hair out. Delphi, protected mode, the library dealing with real mode data. Running the code it would normally segment fault but occasionally work correctly. Single-stepping in the debugger would work correctly 100% of the time.
The library turned out to be riddled with the bug, they were using pointers to point to the real mode addresses. The mere act of loading an invalid pointer is a segment fault and the code emitted by the compiler would copy the pointers by loading them. Note that they were not being followed, if they pointed to nonsense it didn't matter, but if it wasn't a valid address, boom.
Somehow the debugger was successfully executing the invalid load when single stepping. I never investigated exactly what it was up to, I figure it might have been simulating the command to avoid having to write a breakpoint into memory that conceivably could have been read only.
After this formative experience, it took me years to stop instinctually assuming that I was less error prone than the compiler.
I begin to understand the people who write their own libc for security reasons.
int my_memcmp(const void * ptr1, const void * ptr2, size_t num) {
const unsigned char * p1 = ptr1;
const unsigned char * p2 = ptr2;
for (size_t i = 0; i < num; i++) {
if p1[i] < p2[i] {
return -1;
}
if p1[i] > p2[i] {
return 1;
}
}
return 0;
}
For example, technically speaking, unsigned char can be as wide as an int, so "p1[i] - p2[i]" may actually evaluate to unsigned int which is not what you want.> unsigned char can be as wide as an int, so "p1[i] - p2[i]" may actually evaluate to unsigned int which is not what you want
Would this matter?
No, it is not true for almost all non-embedded systems. And char cannot be as wide as int, it is the opposite true, int can be as wide as char.
I use Pascal, because it is very fast and has automated reference counting (ARC) for most types, so it is almost memory safe. It was the only way to get C-like speed, fast compilation and no memory issues. With ARC you never get uninitialized values, you never get a use-after-free, and you never get a double-free.
A few days ago, I ran my program in valgrind's memcheck: double-free detected
That was hard to debug. Valgrind told me where the value was created, but not where it was freed the first time.
Turns out, FreePascal just put an unmatch refcount decrement after a string comparison: https://github.com/benibela/internettools/commit/4c510e8c977...
Time to update FreePascal. I use "nightly" builds of FreePascal because the stable version did not have Android aarch64 support (although the newest stable does support it. but there were other issues with the standard library). Last time I tried to update it, it stopped working on Android x86 and some floating point computations failed. But those issues have been fixed now.
--
Even worse than compiler bugs are CPU bugs. Or emulator bugs if you run it on an emulated CPU. I just had the problem that my app did not start on the aarch64 Android emulator. JNI ExceptionCheck always returned 1. This function here: https://github.com/benibela/internettools/commit/d9fafc9274a... Two instructions added to the assembly and it is fixed and returns 0, but those instructions should not have changed anything
The issue was that extern C functions didn’t have proper type checking of the parameters. The header said one thing, the cpp file something else, and they were very subtly different. The compiler didn’t complain. At runtime the caller would pass some values and the callee would get complete garbage, but only on 64-bit architectures. I tested on 32-bit at the time and never saw it myself until it was too late.
To be fair this one is mostly my own bug. I call it a compiler bug too because I expected the compiler to have my back and it didn’t.
This happens because somehow it uses old cached intermediate files that are no longer valid. No matter what i do it refuses to build it correctly. Even clean and rebuild does not work, it forces me the delete project files and recreate them
It probably an err on our side, especially aince clean dowsnt work. But it is quite annoying when it happens and gets hard to figure out what is going on
Visual Studio's build system is horrible. The compiler and msbuild are actually quite ok if using the command line.
I ran into this with MSVC just a few months back. After an update of the compiler, a bizarre display issue emerged with lines criss-crossing the screen. Turning off optimizations to debug made the problem go away. Eventually tracked down the issue to the following line of code:
if (abs(delta.y) > abs(delta.x)) { …
changing this to:
bool yDominant = abs(delta.y) > abs(delta.x); if (yDominant) { …
fixed the problem. Yikes!
I don't know if they fixed the optimization in question by now.
[0] https://www.phoronix.com/scan.php?page=news_item&px=MTg3OTQ
it turns out that gcc was compiling with an opcode that was invalid on this particular architecture, causing the application to crash in weird ways. being pre-egcs, it was easy to track down and fix by changing the opcode to two instructions instead, fixing the problem.
it was then I understood why this very cool (to me) machine with a fast multi-cpu and high memory had been retired: it's hard to reliably run binaries that you compile that crash randomly, and I seemed to have been the only one to take the time to figure out why.
I have a healthy skepticism of new compiler features as a result.
1. If anyone has significant experience or even just interest in this, they should work to collaborate on how compiler bugs could be taken advantage of for unintended purpose. Otherwise, there will continue to be a lot at risk.
2. You think compiler bugs are bad? How about hardware hacking, quantum effects, etc.
Similar to Schrödinger's cat, security is relative, never absolutely exists, but always exists. It's a Platonic ideal.
"Maturity" of understanding the nature of security is variable and multi-state, like a spiritual journey; greater understanding of security may lead onto to great loss of faith or you may go beyond into the light of true awakening. Most will only get a firewall, though.