Yeah I agree. This is a classic “uninitialized variable has garbage memory value” bug. But it is not a “undefined nasal demons behavior” bug.
That said, we all learn this one! I spent like two weeks debugging a super rare desync bug in a multiplayer game with a P2P lockstep synchronous architecture.
Suffice to say I am now a zealot about providing default values all the time. Thankfully it’s a lot easier since C++11 came out and lets you define default values at the declaration site!
I prefer language constructs define that new storage is zero-initialized. It doesn't prevent all bugs (i.e. application logic bugs) but at least gives deterministic results. These days it's zero cost for local variables and near-zero cost for fields. This is the case in Virgil.
That makes things worse if all-zero is not a valid value for the datatype. I'd much prefer a set-up that requires you to initialise explicitly. Rust, for example, has a `Default` trait that you can implement if there is a sensible default, which may well be all-zero. It also has a `MaybeUninit` holder which doesn't do any initialisation, but needs an `unsafe` to extract the value once you've made sure it's OK. But if you don't have a suitable default, and don't want/need to use `unsafe`, you have to supply all the values.
I think it's acceptable to leave an escape hatch for these situations instead of leaving it to easy to misunderstand nooks and crannies of the standard.
You don't want to zero out the memory?
Slap a "foo = uninitialized" in there to have that exact behavior and get the here be demons sign for free.
Yeah this issue is super obvious and non-controversial.
Uninitialized state is totally fine as an opt-in performance optimization. But having a well defined non-garbage default value should obviously be the default.
Did C fuck that up 50 years ago? Yeah probably. They should have known better even then. But that’s ok. It’s a historical artifact. All languages are full of them. We learn and improve!
I don't know, I expect all variables to be uninitialized until proven otherwise. It makes it easier for me to reason about code, especially convoluted code. But I also like C a lot and actually explicitly invoke UB quite often, so there is that.
I like C and it's great. I wish more people wrote C instead of C++. But there's a reason that literally no modern language makes this choice.
If uninitialization was opt-in you would still be free to "assume uninitialized until proven otherwise". But uninitialized memory is such a monumental catastrophic footgun that really is not a justifiable reason to make that default behavior. Which, again, is why no modern languages make that (terrible) design choice.
There are non-standard mechanisms to control variable initialization. GCC has -ftrivial-auto-var-init=zero for zero-init of locals (with some caveats). For globals, you can link them into a different section than bss to disable zero-init.
I am talking about random convoluted code, I did neither wrote nor control. The UB does not only help the compiler, it also helps me the reverse engineer, since I also can assume that an access without a previous write is either a bug, or I misinterpreted the control flow.
You can assume whatever initialization you want when reading code, even if it's not in the standard. Is your concern that people would start writing code assuming zero-init behavior (as they already do)?
That purpose would be better served by reclassifying uninitialized reads as erroneous behavior, which they are for C++26 onwards. What useful purpose is served by having them be UB specifically?
> Is your concern that people would start writing code assuming zero-init behavior (as they already do)?
Yes, I couldn't assume that such code can be deleted safely. Not sure, if people really rely on it, given that it doesn't work.
> erroneous behavior
So they finally did the thing and made the crazy optimizations illegal?
> If the execution of an operation is specified as having erroneous behavior, the implementation is permitted to issue a diagnostic and is permitted to terminate the execution of the program.
> Recommended practice: An implementation should issue a diagnostic when such an operation is executed.
[Note 3: An implementation can issue a diagnostic if it can determine that erroneous behavior is reachable under an implementation-specific set of assumptions about the program behavior, which can result in false positives.
— end note]
I don't get it at all. The implementation is already allowed to issue diagnostics as it likes including when the line number of the input file changes. In the case of UB it is also permitted to emit code, that terminates the program. This sounds all like saying nothing. The question is what the implementation is NOT allowed to do for erroneous behaviour, that would be allowed for undefined behaviour.
Also if they do this, does that mean that most optimizations are suddenly illegal?
Well, yeah the compiler can assume UB never happens, optimizes and that can sometimes surprise the programmer. But I the programmer also program based on that assumption. I don't see how defining all the UB serves me.
UB doesn't mean there will be nasal demons. It means there can be nasal demons, if the implementation says so. It means the language standard does not define a behavior. POSIX can still define the behavior. The implementation can still define the behavior.
Plenty of things are UB just because major implementations do things wildly differently. For example:
realloc(p, 0)
Having initialization be UB means that implementations where it's zero cost can initialize them to zero, or implementations designed for safety-critical systems can initialize them to zero, or what have you, without the standard forcing all implementations to do so.
> UB doesn't mean there will be nasal demons. It means there can be nasal demons, if the implementation says so.
Rather "if the implementation doesn't say otherwise".
Generally speaking compiler writers are not mustache-twirling villains stroking a white cat thinking of the most dastardly miscompilation they could implement as punishment. Rather they implement optimisation passes hewing as close as they can to the spec's requirements. Which means if you're out of the spec's guarantees you get whatever emergent behaviour occurs when the optimisation passes run rampant.
All of that implementation freedom is also available if the behavior is erroneous instead. Having it defined as UB just gets you nasal demons, which incidentally this rule leads to on modern compilers. For example:
Yeah that’s just really bad language design. Which, again, literally no modern languages do because it’s just terrible horrible awful no good very bad design.
It's describing rather than prescribing, which yeah isn't really design. Most modern languages don't even (plan to) have multiple implementations, much less a standard.
In terms of compile times, boost geometry is somehow worse. You're encouraged to import boost/geometry.hpp, which includes every module, which stalls compile times by several seconds just to parse all the templates. It's not terrible if you include just the headers you need, but that's not the "default" that most people use.
If you have 25gb of executables then I don’t think it matters if that’s one binary executable or a hundred. Something has gone horribly horribly wrong.
I don’t think I’ve ever seen a 4gb binary yet. I have seen instances where a PDB file hit 4gb and that caused problems. Debug symbols getting that large is totally plausible. I’m ok with that at least.
Llamafile (https://llamafile.ai) can easily exceed 4GB due to containing LLM weights inside. But remember, you cannot run >4GB executable files on Windows.
> A few ps3 games I've seen had 4GB or more binaries.
Is this because they are embedding assets into the binary? I find it hard to believe anyone was carrying around enough code to fill 4GB in the PS3 era...
I assume so, there were rarely any other files on the disc in this case.
It varied between games, one of the battlefields (3 or bad company 2) was what I was thinking of. It generally improved with later releases.
The 4GB file size was significant, since it meant I couldn't run them from a backup on a fat32 usb drive. There are workarounds for many games nowadays.
Not quite. I very much work in large, templated, C++ codebases. But I do so on windows where the symbols are in a separate file the way the lord intended.
Evidence of no exploitations? It's usually hard to prove a negative, except when you have all the logs at your fingertips you can sift through. Unless they don't, of course. In which case the point stands: they don't actually know at this point in time, if they can even know about it at all.
Specifically, it looks like the exflitration primitive relies on errors being emitted, and those errors are what leak the data. They're also rather characteristic. One wouldn't reasonably expect MongoDB to hold onto all raw traffic data flowing in and out, but would absolutely expect them to have the error logs, at least for some time back.
I feel like that's an issue not with what they said, but what they did. It would be better for them to have checked this quickly, but it would have been worse for them to have they did when they hadn't. What you're saying isn't wrong, but it's not really an answer to the question you're replying to.
> "No evidence of exploitation” is a pretty bog standard report
It is standard, yes. The problem with it as a statement is that it's true even if you've collected exactly zero evidence. I can say I don't have evidence of anyone being exploited, and it's definitely true.
It's not really my bar, I just explored this on behalf of the person you were replying to because I found it mildly interesting.
It is also a pretty standard response indeed. But now that it was highlighted, maybe it does deserve some scrutiny? Or is saying silly, possibly misleading things okay if that's what everyone has always been doing?
Timesync isn’t a nightmare at all. But it is a deep rabbit hole.
The best approach, imho, is to abandon the concept of a global time. All timestamps are wrt a specific clock. That clock will skew at a rate that varies with time. You can, hopefully, rely on any particular clock being monotonous!
My mental model is that you form a connected graph of clocks and this allows you to convert arbitrary timestamps from any clock to any clock. This is a lossy conversion that has jitter and can change with time. The fewer stops the better.
I kinda don’t like PTP. Too complicated and requires specialized hardware.
This article only touches on one class of timesync. An entirely separate class is timesync within a device. Your phone is a highly distributed compute system with many chips each of which has their own independent clock source. It’s a pain in the ass.
You also have local timesync across devices such as wearables or robotics. Connecting to a PTP system with GPS and atomic clocks is not ideal (or necessary).
> I kinda don’t like PTP. Too complicated and requires specialized hardware.
At this stage, it's difficult to find an half-decent ethernet quality MAC that doesn't have PTP timestamping. It's not a particularly complicated protocol, either.
I needed to distribute PPS and 10MHz into a GNSS-denied environment, so last summer I designed a board to do this using 802.1AS gPTP with a uBlox LEA-M8T GNSS timing receiver, a 10MHz OCXO and an STM32F767 MCU. This took me about four weeks. Software is written in C, and the PTP implementation accounts for 1500 LOC.
> I kinda don’t like PTP. Too complicated and requires specialized hardware.
In my view the specialised hardware is just a way to get more accurate transmission and arrival timestamps. That's useful whether or not you use PTP.
> My mental model is that you form a connected graph of clocks and this allows you to convert arbitrary timestamps from any clock to any clock. This is a lossy conversion that has jitter and can change with time.
This sounds like the "peer to peer" equivalent to PTP. It would require every node to maintain state about it's estimate (skew, slew, variance) of every other clock. I like the concept, but obviously it adds complexity to end-stations beyond what PTP requires (i.e. increases the hardware cost of embedded implementations). Such a system would also need to model the network topology, or control routing (as PTP does), because packets traversing different routes to the same host will experience different delay and jitter statistics.
> TicSync is cool
I hadn't seen this before, but I have implemented similar convex-hull based methods for clock recovery. I agree this is obviously a good approach. Thanks for sharing.
> This sounds like the "peer to peer" equivalent to PTP. It would require every node to maintain state about it's estimate (skew, slew, variance) of every other clock.
Well, it requires having the conversion function for each edge in the traversed path. And such function needs to exist only at the location(s) performing the conversion.
> obviously it adds complexity to end-stations beyond what PTP requires
If you have PTP and it works then stick with it. If you’re trying to timesync a network of wearable devices then you don’t have PTP stamping hardware.
> because packets traversing different routes
Fair callout. It’s probably a more useful model for less internty use cases. Of which there are many!
For example when trying to timesync a collection of different sensors on different devices/microcontrollers.
Roboticists like CanBus and Ethercat. But even that is kinda overkill imho. TicSync can get you tens of microseconds of precision in user space.
Is there a Clang based build for Windows? I’ve been slowly moving my Windows builds from MSVC to Clang. Which still uses the Microsoft STL implementation.
So far I think using clang instead of MSVC compiler is a strict win? Not a huge difference mind you. But a win nonetheless.
reply