How does binary I/O of POD types not break the aliasing rules?

Strict aliasing is about accessing an object through a pointer/reference to a type other than that object's actual type. However, the rules of strict aliasing permit accessing any object of any type through a pointer to an array of bytes. And this rule has been around for at least since C++14.

Now, that doesn't mean much, since something has to define what such an access means. For that (in terms of writing), we only really have two rules: [basic.types]/2 and /3, which cover copying the bytes of Trivially Copyable types. The question ultimately boils down to this:

Are you reading the "the underlying bytes making up [an] object" from the file?

If the data you're reading into your s was in fact copied from the bytes of a live instance of S, then you're 100% fine. It's clear from the standard that performing fwrite writes the given bytes to a file, and performing fread reads those bytes from the file. Therefore, if you write the bytes of an existing S instance to a file, and read those written bytes to an existing S, you have perform the equivalent of copying those bytes.

Where you run into technical issues is when you start getting into the weeds of interpretation. It is reasonable to interpret the standard as defining the behavior of such a program even when the writing and the reading happen in different invocations of the same program.

Concerns arise in one of two cases:

1: When the program which wrote the data is actually a different program than the one who read it.

2: When the program which wrote the data did not actually write an object of type S, but instead wrote bytes that just so happen to be legitimately interpret-able as an S.

The standard doesn't govern interoperability between two programs. However, C++20 does provide a tool that effectively says "if the bytes in this memory contain a legitimate object representation of a T, then I'll return a copy of what that object would look like." It's called std::bit_cast; you can pass it an array of bytes of sizeof(T), and it'll return a copy of that T.

And you get undefined behavior if you're a liar. And bit_cast doesn't even compile if T is not trivially copyable.

However, to do a byte copy directly into a live S from a source that wasn't technically an S but totally could be an S, is a different matter. There isn't wording in the standard to make that work.

Our friend P0593 proposes a mechanism for explicitly declaring such an assumption, but it didn't quite make it into C++20.