The trouble with struct sockaddr's fake flexible array

76 pointsposted 15 hours ago
by signa11

41 Comments

jandrese

2 hours ago

It wouldn’t help the kernel case here, but I’ve long thought that the socket TCP and UDP APIs were bad in the general use case. Having to build and shuffle around the sockaddr structure is too low level for the majority use case. I think most programmers would prefer something like:

    int inet_connect(AF_TYPE, destination, port, options, &error);
Also:

    int inet_listen(AF_TYPE, port, options, &error);
So for example:

    int sock;
    inet_err err = NULL;
    sock = inet_connect(SOCK_STREAM, “google.com”, 443, NULL, &err);
    if ( sock < 0 )
    {
        printf(“Failed to connect: %s\n”, err->message);
        return -1;
    }
Basically one level more abstraction in the API. The old API could stick around for people who are doing something weird, but this would cover most use cases I think. Added bonus: programs using this API are trivial to add support for IPv6, it is just a recompile.

asveikau

2 hours ago

> sock = inet_connect(SOCK_STREAM, “google.com”, 443, NULL, &err);

You just added a DNS lookup to connect(2).

Also, you linked socket creation to all of this. This would make it hard to do an asynchronous version. Today you can create a socket and do a non-blocking connect(2) (after doing your asynchronous DNS lookup)

Lastly, I think if you tasked someone with creating that API, they would probably use the old APIs to build it ...

ape4

2 hours ago

That second parameter could accept a text IP-address eg "192.168.100.100". Like the address bar of a browser.

braiamp

an hour ago

So, the dns lookup is still there, it's just optional. You would have to pass a flag to tell it not to try to do the lookup or you would have to do something fancy trying to know if the passed parameter is an IP.

asveikau

an hour ago

This is already how all the DNS lookup APIs work.

kazinator

8 hours ago

I believe struct sockaddr was supposed to serve the role for which sockaddr_storage was later invented. That's why it has the padding array sa_data: so that if you actually define or allocate such an object it will be big enough to hold any address. In no way is that a flexible array, and you never want to be accessing it.

Probably, sockaddr_storage was introduced rather than making sockaddr's array bigger because by that time the definition of sockaddr had become a mystical sacred cow, not to be touched. Plus that size of 14 is the same on every system. It is a one size fits all approach which is not a way to define the maximum size structure over all the address types if you want every platform to be able to make it a tight fit. The size is going to vary with implementation due to different alignment and padding requirements.

OptionOfT

3 hours ago

> Needless to say, these casts can be error prone; casting a pointer between different structure types is also deemed to be undefined behavior in current C.

This surprised me. What kind of mechanism is there in place to prevent the compiler from formatting /dev/sda when you write code like this?

NobodyNada

an hour ago

This is one of the most unfortunate cases of UB in C. It's necessary because C largely doesn't forbid pointer aliasing (accessing an object using multiple, distinct pointers). But the compiler needs to have some idea of when pointers might alias; otherwise, it's forced to generate extremely inefficient code that redundantly loads pointers over and over again just in case the value changed unexpectedly.

Within the body of a single function, the compiler can see where addresses came from and figure out what pointers may or may not alias. But when pointers are passed in as function parameters, the compiler has no idea. Thus, the C standard allows compilers to assume that pointers to different types never refer to the same object; an assumption that's true for almost all code anyone would want to write. But this adds surprising undefined behavior to some things you might try, like casting between layout-compatible structs, or the fast inverse square root trick from Quake. (Casting a pointer to a different type can still be tricky to get right even if it wasn't UB though. Alignment requirements are another footgun: for example, it's not legal to cast a uint8_t pointer to a uint16_t pointer unless you're sure its address is even.)

The blessed-by-the-standard way to do type punning is to either use a union or a memcpy, not a pointer cast.

For comparison, Rust prohibits mutable pointer aliasing for safe reference types, so the compiler knows when references may or may not alias. (Raw pointers are assumed to always be aliasable unless the compiler can prove that they're unique, e.g. by being derived from a safe reference). This leads to more efficient codegen in many cases, but it also means that type punning through pointer casting is fully legal in unsafe code (provided validity and alignment requirements are met), since the compiler does not need the type information to figure out aliasing.

kevin_thibedeau

34 minutes ago

> But when pointers are passed in as function parameters, the compiler has no idea.

"restrict" was added to give the compiler an idea.

aw1621107

2 hours ago

As far as standard ISO C goes, nothing. That's just how UB can work in theory, though in practice compiler (ab)use of UB is somewhat more indirect (optimizing under the assumption that UB is not present in the program, rather than observing UB and making decisions based on that).

Beyond that, I believe Linux and other kernels technically use a slight variant of C by taking advantage of compiler extensions/flags to better fit their use cases. For example, Linux compiles (compiled?) with -fno-strict-aliasing and -fwrapv and uses a GCC extensions that allows type punning via unions [0], so that they can compile what the standard calls "incorrect" C code without worry.

I'm not sure whether recent versions of C have changed their stance on this particular UB, but since it'll probably be a while until they're adopted (if ever) kernels will be making do with their workarounds for a while longer.

[0]: https://lkml.org/lkml/2018/6/5/769

Conscat

2 hours ago

Type punning through unions is not undefined behavior in C, only in C++.

aw1621107

an hour ago

Yep, you're right. Think I got confused with something else :(

acuozzo

an hour ago

It's not UB if the "common initial sequence" exemption applies as it does here.

cratermoon

3 hours ago

Nothing in the language, though the permissions mechanism of the system may disallow it. Unless the compiler is running with root permissions.

C is not a safe language. The more abrasive among fans of the language would say "skill issue" and "git gud" if you want to avoid footguns.

sseagull

2 hours ago

What kind of mechanism is there in place to prevent any compiler from just replacing your main() function with formatting /dev/sda?

That's nothing to do with the language, that's always on the compiler. Nothing stops the compiler from taking even correctly-specified code and doing whatever it wants.

ivanbakel

an hour ago

That's a rathee obtuse response. You can typically operate under the assumption that the compiler works correctly. The question of the GP was - what prevents the compiler from (correctly) exploiting UB to change the program behaviour from the desired one? If the Linux devs want to rely on their compilers' output, they have to somehow be obeying the contract around UB.

shawnz

25 minutes ago

I think the more reasonable assumption is that the practical needs of the biggest users will probably trump what any specification demands.

The thing stopping the compiler from doing dangerous behaviours in response to commonly abused UB is obviously that people wouldn't use the compiler if it did that. Just like how the thing stopping the compiler from doing dangerous behaviours in response to spec-legal code is that people wouldn't use it if it did that.

gosub100

an hour ago

he was referring to the U in "UB".

dataflow

8 hours ago

Here's what I don't understand:

> As long as this usage remains, the checking tools built into both compilers must treat any trailing array in a structure as if it were flexible; that can disable overflow checking on that array entirely.

No, they don't? Why couldn't they just have a mechanism to suppress the check for this particular struct (like a whitelist)?

AshamedCaptain

5 hours ago

I'd think this is an extremely common pattern anyway (with the declared array length been 1, or even some other "minimal storage value"), and likely struct sockaddr's use of it is the cherry on the top.

poincaredisk

5 hours ago

It is in C, but not in the Linux kernel (according to TFA). That's why they're planning a big refactoring to get rid of this structure from the kernel

dataflow

5 hours ago

The only embedded sizes I've ever seen are flex, 0, 1, and 14 (sockaddr's). It's trivial enough to exclude all of them.

AshamedCaptain

4 hours ago

No, I have seen many people use an arbitrary value to indicate "this is the amount it makes most sense to allocate this structure with", e.g. when you allocate it on the stack. If you need more than that you allocate it with a malloc wrapper or the like, which returns one of arbitrary long size.

What I have not seen is this happening in the middle of the struct, for obvious reasons; it's always the last element in the struct.

dataflow

3 hours ago

> No, I have seen many people use an arbitrary value

Have you seen that in something that's ABI-critical, though? i.e. whose code simply cannot be changed due to backward compatibility, like is the case with sockaddr? Because otherwise I'd consider it a non-issue.

AshamedCaptain

2 hours ago

Everything is an ABI issue. Dunno what the point or alternative is here.

dataflow

2 hours ago

No? Not every struct is exposed to clients who can't change their code.

oersted

6 hours ago

Yeah it's interesting that a (relatively) small quirk in the Linux source has the "political leverage" to impact the whole C ecosystem.

I suppose C was made for Unix at Bell Labs and GCC is also inextricably tied, you could say that C is first and foremost the language of Linux, fair enough.

nine_k

2 hours ago

I suspect that the problem is a problem in many code bases, but only a hugely important project like Linux has the leverage to try and have it addressed at the ecosystem level.

arisudesu

11 hours ago

Either I don't understand the problem completely, or why wasn't it possible to introduce something like 'ex' address family that allowed to pass and disambiguate extended parameter format(s) which would include array sizes etc? We had these *Ex functions everywhere in Win32 API for an eternity, why unices couldn't do the same trick?

kazinator

8 hours ago

You don't need the Ex thing with socket addresses because they have a run-time type field: sa_family.

A function that takes a sockaddr * can dereference it to get at that field, and then know exactly which address type it's dealing with.

dblohm7

4 hours ago

Respectfully, I think you're missing the point. Windows uses idioms like that too (though usually via filling an initial field with the size of the structure in byte), but GP's point of an Ex-style API would be to completely eliminate kludges such as runtime type fields at the beginning of structs and move to something safer.

dwattttt

10 hours ago

You can introduce new APIs and new types to resolve the "is it flexible, or is it 14 chars"; that's more or less one of the explored approaches in the article.

It won't make existing uses any clearer or safer though, requiring rewrites to take advantage of them.

kazinator

8 hours ago

The array should not be touched so the question is moot. The struck sockaddr type should only be used for pointers, which are cast to the correct type according to their family before they're are dereferenced, with the exception that the sa_family member can be accessed through the sockaddr base.

For defining or allocating an object that can hold any address, sa_storage should be used, mentioned in the article.

dwattttt

7 hours ago

I imagine the problem they're trying to address is ensuring that everyone _does_ only use correctly cast pointers; as defined, it's legal to use that 14 char array, it's just that it's never what you're meant to do.

Retr0id

9 hours ago

If the goal here is to reduce attack surface or bug-prone code in the kernel, leaving the non-ex variants around unchanged wouldn't help much with that.

rjsw

6 hours ago

The article could point out that real BSD sockaddrs only have an 8-bit field for the address family which leaves another 8 bits for the length of the structure.

asveikau

2 hours ago

Some platforms have a field called sa_len.

pm2222

4 hours ago

Why not just use TLV style structure?

asveikau

2 hours ago

That's essentially what it is.

You can determine the size by looking at sa_family.

Some platforms also have an sa_len field.

It's still unsafe if you allocate for the "base" type of struct sockaddr then try to use it for something larger, but that generally is not done. People usually allocate for the exact type they want, and only pointers to the structure are passed around, often opaquely.