5 ms·
Fun with -fsanitize=undefined and Picolibc
- nasretdinov 2y ago> String to float conversion had a table missing four values. This caused an array access overflow which resulted in imprecise values in some cases. I've once wrote a function to parse the date format from log files that Go doesn't natively support, and forgot to add November. I quit that job in April, so I never saw any issues. However when 1st of November came my ex-colleagues saw no logs for this day, and when they found out the reason they created a hash tag #nolognovember which you can probably find somewhere to this day :)
- lionkor 2y agoWho needs unit tests when you have "squints lgtm"
- Joker_vD 2y agoPfft, you know how tests for functions like this: func MonthToString(month int) string { switch month { case 1: return "January" case 2: return "February" ... case 10: return "October" case 12: return "December" default: panic(fmt.Errorf("invalid month number: %d", month)) } } are usually written? You take the switch's body, shove it into the test function, and then replace "case/return" with regexp to "assert.Equal" or something: func TestMonthToString(t *testing.T) { assert.Equal(t, "January", MonthToString(1)) assert.Equal(t, "February", MonthToString(2)) ... assert.Equal(t, "October", MonthToString(10)) assert.Equal(t, "December", MonthToString(12)) assert.PanicsWithError(t, "invalid month number: 13", func() { MonthToString(13) }) } Look ma, we got that sweet 100% code coverage!
- BobbyTables2 2y agoI feel attacked! (:>
- nasretdinov 2y agoNice nick name. What happened to the first one?
- BobbyTables3 2y agoIt got dropped.
- nasretdinov 2y agoAgain??
- gus_massa 2y agoThe solution is to add integration testing, for (i=1, i<13, i++) { assert.Equal(t, i, StringToMonth(MonthToString(i))) } the reverse composition is harder to test.
- debugnik 2y agoDid you mean property testing?
- wavemode 2y agoThis is kind of bug unit tests don't catch. The only way to exhaustively test that you're handling every possible input, is to loop over every possible input (which may tend to infinity). Therein lies the importance of runtime assertions (so we can sanity check that parsing actually succeeded rather than silently failing) and monitoring (so we can sanity check that, for example, we don't ever go 24 hours without receiving data from the parsing job).
- kccqzy 2y ago> to loop over every possible input (which may tend to infinity). This attitude is defeatist. The success of property based testing (see QuichCheck in Haskell or Hypothesis in Python) especially when combined with fuzzing shows that instead of looping over every possible input, looping over thousands of inputs tend to be good enough in practice to catch bugs. Throwing infinity as a cop out is a lazy opinion held by people who don't understand infinity, or rather, the concept of infinity that's countable. Everything we model on a computer is countably infinite. When we have multiple of such countable infinite sets, the standard dovetail constructions guarantees that their union will be countable. Their Cartesian product will also be countable. You can always obtain an interesting prefix of such an infinite set for testing purposes.
- wavemode 2y agoYour tone implies to me that you are under the impression that I'm suggesting one should not test their software. Nothing could be further from the truth. What I'm saying is that it's foolish not to take any measures at runtime to validate that the system is behaving correctly. Who's to say that the logs themselves are even formatted correctly? Your software could be perfectly bug-free and you'd still have problems without knowing it, due to bugs in some other person's software. That's the point you're missing - no matter how many edge cases you account for, there's always another edge case.
- kccqzy 2y agoOh no not at all. I didn't imply that you are suggesting one shouldn't test their software. Instead, I believe you have an overly narrow view of what tests can accomplish. I didn't say anything about measures at runtime to validate things. That's complementary to good tests.
- nasretdinov 2y agoWell I guess you can have a unit test that checks that there are 12 entries :). Repeating the same list probably wouldn't have found an issue unfortunately
- bad_username 2y ago> when 1st of November came my ex-colleagues saw no logs for this day Faced with this symptom I would bet there was a "No" in a yaml somewhere :-)
- juliangmp 2y ago> [...] detect places where the program wanders into parts of the C language specification [...] Small nitpick, the UB sanitizer also has some checks specific for C++ https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
- zero_k 2y agoWow, this: "random() was returning values in int range rather than long." is a very nice bug find. Randomness is VERY hard to check for humans. For example, Python's binomial distribution is very bad on some inputs [1], giving widely wrong values, but nobody found it. I bumped into it when I implemented an algorithm to compute the approximate volume of solutions to a DNF, and the results were clearly wrong [2]. The algorithm is explained here by Knuth, in case you are interested [3] [1] https://www.cs.toronto.edu/~meel/Slides/meel-distform.pdf https://www.cs.toronto.edu/~meel/Slides/meel-distform.pdf [2] https://github.com/meelgroup/pepin https://github.com/meelgroup/pepin [3] https://cs.stanford.edu/~knuth/papers/cvm-note.pdf https://cs.stanford.edu/~knuth/papers/cvm-note.pdf
- bestouff 2y ago> the vast bulk of sanitizer complaints came from invoking undefined or implementation-defined behavior in harmless ways This is patently false. Any Undefined Behavior is harmful because it allows the optimizer to insert totally random code, and this is not a purely theoretical behavior, it's been repeatedly demonstrated happening. So even if your UB code isn't called, the simple fact it exists may make some seemingly-unrelated code behave wrongly.
- almostgotcaught 2y ago> optimizer to insert totally random code What are you even saying - what is your definition of "random code". FYI UB is exactly (one of) the places where an optimizer can insert optimized code.
- moefh 2y agoNot OP, but here's an example of "random code" inserted by the compiler[1]: note the assembly instruction "ud2" ("invalid opcode exception" in x86 land) instead of "ret" in not_ok(). You might think this code would be fine if address 0 were mapped to RAM, but both gcc and clang know it's undefined behavior to use the null pointer like that, so they add "random code" that forces a processor exception. [1] https://godbolt.org/z/sK55YsGz1 https://godbolt.org/z/sK55YsGz1
- anamexis 2y agoThat doesn't sound very random to me!
- flohofwoe 2y agoUnfortunately UB is an umbrella term for all sorts of things, and some of those can be very harmful/unexpected, while others are (currently) harmless - but that may change in new compiler versions. The typical optimization showcase (better code generation for signed integer loop counts) only works when the (undefined behaviour) signed integer overflow doesn't actually happen (e.g. the compiler is free to assume that the loop count won't overflow). But when the signed integer overflow happens all bets are off what will actually happen to the control flow - while that same signed integer overflow in another place may simply wrap around. Another similar example is to specifically 'inject' UB by putting a `std::unreachable` into the default case of a switch statement. This enables an optimization that the compiler omits a range check before accessing the switch-case jump table. But if the switch-variable isn't handled in a case-branch, the jump table access may be out-of-bounds and there will be a jump to a random location. In other situations the compiler might even be able to detect at compile time that the UB is triggered and simply generate broken code (usually optimizing away some critical part), or if you're lucky the compiler inserts an ud instruction which crashes the process.
- moefh 2y ago> Passing pointers to the middle of a data structure. For example, free takes a pointer to the start of an allocation. The management structure appears just before that in memory; computing the address of which appears to be undefined behavior to the compiler. To clarify, the undefined behavior here is that the sanitizer sees `free` trying to access memory outside the bounds of what was returned by `malloc`. It's perfectly valid to compute the address of a struct just before memory pointed to by a pointer you have, as long as the result points to valid memory: void not_free(void *p) { struct header *h = (struct header *) (((char *)p) - sizeof(struct header)); // ... } In the case of `free`, that resulting pointer is technically "invalid" because it's outside what was returned by `malloc`, even though the implementation of `malloc` presumably returned a pointer to memory just past the header.
- josephg 2y agoYeah; I used to enjoy poking through C code in well written programs. It’s amazing what gems you can find by people who really know their stuff. I saw a string library once which took advantage of this. The library passed around classic C style char* pointers. They work in printf, and basically all C code that expects a string. But the strings had extra metadata stored before the string content. That metadata contained the string’s current length and the total allocation size. As a result, you could efficiently get a string length without scanning, append to a string, and do all sorts of other useful things that are tricky to do with bare allocations. All while maintaining support for the rest of the C ecosystem. It’s a very cool trick!
- alexvitkov 2y agoThe Windows API uses this scheme for one of its 50 string types [1]. I"m not very fond of this design as it's easy to pass a "normal" C string, which compiles because BSTR is just a typedef to it. You can allocate the exact same data structure, but store a pointer to the size prefix, instead of the first byte - you avoid that issue, and can still pass the data field to anything expecting a zero-terminated string: struct WeirdString { int size; char data[0]; }; struct WeirdString* ws = ...; fopen(ws->data); [1] BSTR - https://learn.microsoft.com/en-us/previous-versions/windows/desktop/automat/bstr https://learn.microsoft.com/en-us/previous-versions/windows/...
- Arnavion 2y agoThat arithmetic shift right implementation is also what I came up with for a video game fantasy architecture that only has logical shift right. (16-bit registers) ; asr rd, rs1, rs2 ; rd = signed(rs1) >> rs2 and rt, rs1, 0x8000 ; isolate sign bit lsr rt, rt, rs2 ; shift sign bit to final position neg rt, rt ; sign-extended part of final result lsr rd, rs1, rs2 ; base part of final result or rd, rd, rt ; combine both parts It might be easier to understand broken down this way for anyone who didn't understand the article's one-liner.
- musicale 2y agoAnd don't forget -fbounds-safety, which is in Apple's clang/llvm and perhaps other versions. https://clang.llvm.org/docs/BoundsSafety.html https://clang.llvm.org/docs/BoundsSafety.html
- deleted 2y ago[deleted]