But yeah, I should have mentioned the caveats too. I will update the post once I'm back at my computer.
So if you're not working on an OS kernel somewhere in the syscall path - really not a common activity even among C programmers - it's not a problem. If you are passing a struct from one part of your program to another part, or to a library you use which is not isolated or sandboxed in some way, it's not a problem. If you pass a struct to a _more_ privileged context like a syscall, it's not a problem. If you are passing structs over a network connection or writing to a file, you'll pack them and be aware of every byte anyway, in order to be compatible with another implementation reading them.
If you _are_ passing a struct from a more privileged context to a less privileged context, then yeah you have to memset().
Simply not true; that context could pass a copy of that structure to a less privileged context other than the original one, without doing anything about the padding.
(Well, you could argue that it's the privileged kernel's fault: it should meticulously floss the structure between the members to clear the padding to zero while preserving the values.)
Someone upthread mentioned "sending over a network": that's an example. Sending over a network (or local socket/pipe) begins with pushing it down to a more privileged context, which then assiduously places all the bytes into a buffer that is blasted out on the wire. In this case, the more privileged context has no idea what the structure even is; it's just a blob of bytes.
_6 When a value is stored in an object of structure or union type, including in a member object, the bytes of the object representation that correspond to any padding bytes take unspecified values. 51)_
So sure, a memcpy after a memset aught to preserve all the bytes, but if they are updated through a struct which contains padding then the values in the padding bytes will be left unspecified.
Even if memset in a vacuum is guaranteed, look at the quote by anyfoo. "When a value is stored in an object of structure or union type, including in a member object, the bytes of the object representation that correspond to any padding bytes take unspecified values."
If the compiler knows you're going to write to the members right after the memset, it can put the padding back to its previous values. And by that I mean as far as your code can tell it put it back, but in actuality it never zeroed the padding to begin with.
And since there are performance benefits to pretending, now you have a situation where a "helpful" compiler and a malicious compiler have the same effect: data can get leaked.
Also, type-punning is a thing. memset is byte-oriented/memory-oriented. Just because you're using it to zero a struct of a particular kind doesn't mean that's the only way the memory will accessed. Just because reading the padding of some struct is undefined behaviour doesn't mean accessing those bits by some other means is also undefined.
A compiler usually can't eliminate a call to memset() in most practical cases of initialization (where memory leakage is also a concern) because they almost always pass a reference to a routine in another translation unit. Something like memzero_explicit() can be used anyway -- but you're massively overstating the relevance of compilers eliminating dead stores done via memset(). It's much more of an issue for post-destruction memory sanitizing (which is the primary use case for memzero_explicit) than it is for compiler f*ckery when memset() is used for initialization.
You would have to call such a function between the memset and the first time you write to a member. Otherwise the compiler is allowed to say "I put the padding back, and you can't prove otherwise".
> Just because reading the padding of some struct is undefined behaviour doesn't mean accessing those bits by some other means is also undefined.
It's not always undefined, but it says very clearly that the value of padding becomes unspecified.
> they almost always pass a reference to a routine in another translation unit
> you're massively overstating the relevance of compilers eliminating dead stores done via memset
Unless inlining happened, or link-time optimization, or, or...
If the compiler zeroes the memory most of the time, that makes it even scarier. Because all your tests come back clean and safe, then four years later a macro changes and suddenly you're leaking data all over the place.
I don't think I'm overstating the relevance at all. Any security feature that could disappear because of a reasonable, trying-to-help optimization is one that should have a bright red warning label. And this is such a feature. It doesn't require a "sufficiently smart" compiler, and it doesn't require a malicious compiler. This is the kind of thing that can break by accident and ruin everyone's month.
Not in practice. Compilers make use of undefined behaviour to optimize things that are widely applicable and profitable. No real compiler does what you're saying and no future compiler is likely to without explicitly being asked to.
I agree that, by the letter of the spec, you're right, but you're still most certainly overstating the relevance.
I'm not arguing that this isn't a real problem or that people shouldn't use memzero_explicit() (or similar) where security is on the line, as I already said several times in another sub-thread -- I'm just saying that this kind of thing is extreme language-lawyering beyond the realms of probability. It's still not an excuse to be lax, but let's be realistic about the actual likelihood of it happening.
It's not just any, typical kind of dead store elimination. It also would require other optimization passes that I can assure you no mainstream compiler actually does. You can disagree with me if you want, but you're simply wrong.
That step is 90% of the work. Once you can do that, it's straightforward to assess that every field is assigned after a memset, with no intervening reads, and then remove the memset.
I spent several years working on a production grade compiler and I can assure you it's not. But keep just making things up off the top of your head if it makes you feel smart.
Let's look at a basic common case, as a checklist.
1. All fields are definitely assigned.
2. No functions are called except intrisics.
3. Nothing reads from the object on any control path.
4. Memset happens between creation and first assignment.
We agreed that step 1 is a solved problem, right? Are any of 2-4 difficult? Did I miss any prerequisites for the optimization?
Once you can prove 1-4, isn't the optimization pass as simple as looking for memsets applied to structs, checking 1-4, then deleting the call?
All "undefined behaviour" means is that the spec can no longer guarantee anything about the execution of a program once a constraint is violated. It doesn't say anything about the practicality of actually doing it on any given implementation.
FWIW I didn't downvote you, but your reasoning about undefined behaviour seems overly simplistic. It should never be used to explain away security concerns.
You can memset to 0, and on your compiler it might be secure, but it's not enough to keep your security guaranteed against future compilers.
There's a reason functions like SecureZeroMemory exist. In a similar situation, you try to prevent leaking secure secrets by zeroing memory before releasing it. But the compiler sees you never use the variable again, and optimizes away the zeroing.
The message to take away is not "be less paranoid". It's "be more paranoid".
But the problem is that the security concerns and fixes are all undefined here.
So the initial comment is still very right. If you're relying on the padding to be anything in particular, you're in trouble.
No one in this sub-thread has mentioned (or implied) any "fix". You appear to be putting words in my mouth.
> So the initial comment is still very right.
Of course it's right, but it's also misleading when read in the context of the parent comment.
> If you're relying on the padding to be anything in particular, you're in trouble.
No shit.
I'm talking about the comments hermitdev was replying to that were treating memset as a 'fix'.
And 'fix' is shorthand for the opposite of "open[ing] you up to all kinds of info leaks". I don't think that's putting words in anyone's mouth.
To be fair though, memset() usually IS a fix. As mentioned by the kernel memzero_explicit() docs:
> usually using memset is just fine (!)
-- https://www.kernel.org/doc/htmldocs/kernel-api/API-memzero-e...
A conforming C compiler can't just remove memset() as it pleases. The case that most often requires memzero_explicit() is when zeroing an object after destruction, because the compiler thinks it can statically determine that it's a dead store. It very rarely happens that a compiler elides a memset() used for initialization.
I'm not sure why you seem to think that memset() can just be dropped at will for no reason whatsoever or that it's somehow always undefined behaviour.
For the sake of argument, can you show me some example code where it would be conforming to remove a memset() call? Preferably a realistic example and not a Google'd copypasta. Because it's all too easy to just regurgitate things you heard and think you understood, but no so easy to demonstrate it yourself.
Dead Store Elimination (Still) Considered Harmful
The compiler could also determine that &a.x)[1] is undefined behavior in C (reading past the end of an object), and take advantage of that.
The way to check that is by disassembling the code, and even that doesn’t say what a C compiler should do, only what these particular compilers do.