简体   繁体   中英

Safely dereferencing pointers to unions of structs in C?

I am trying to write a packet decoder for the SCTP protocol in C and running into some issues while dereferencing a pointer to a union of structs (which represent the SCTP chunks).

Not everyone might be familiar with the SCTP (Stream Control Transmission Protocol), so here are some brief primers:

Stream Control Transmission Protocol
SCTP packet structure
RFC 4960

In a nut shell, after the SCTP Common header are a series of one or more "chunks". Most of the time, each SCTP packet has just one chunk type, but you can have chunk bundling in which certain chunk types can be bundled together into a single packet. Because of this bundling, I can't just define a union inside my SCTP header structure and call it a day.

So this is what I have for struct sctp_header :

struct sctp_header {
    uint16_t srcport;
    uint16_t dstport;
    uint32_t vtag;
    uint32_t chksum;
    uint8_t ctype;
    uint8_t cflags;
    uint16_t clength;
    void *chunks;
};


And for union sctp_chunks (truncated to just two chunk types for this question):

struct sctp_data {
    uint32_t tsn;
    uint16_t stream_id;
    uint16_t stream_seq;
    uint32_t pp_id;
    void *data;
};

struct sctp_init {
    uint32_t initate_tag;
    uint32_t a_rwnd;
    uint16_t num_out_streams;
    uint16_t num_in_streams;
    uint32_t tsn;
    void *params;
};

union sctp_chunks {
    struct sctp_data *data;
    struct sctp_init *init;
};



Right now, I am overlaying sctp_chunks onto sctp_header->chunks (once I have done all the other necessary checks to make sure I am sitting on an SCTP packet). I then read sctp_header->ctype in a switch statement, and based on that, I know whether I can access sctp_header->chunks->data->tsn or sctp_header->chunks->init->initate_tag (after casting to (sctp_chunks *) ), and so on for other chunk types. Later on, I'll do math and check for remaining chunks, and re-overlay the sctp_chunks union onto the remaining data until I have processed all chunks. For now, I am operating on just the first chunk.

Problem is, trying to access data->tsn or init->initiate_tag (or any other member of the union) causes a SIGSEGV. I don't have GDB handy (I am just a user on the machine this is being coded on), so I am having a hard time figuring out why my program is segfaulting. I believe my use of the structures/unions to be sound, but thus is the nature of C, it's probably something REALLY subtle that is hooking me here.

Printing the pointer addresses for chunks->data or chunks->init shows me addresses that do not appear to be NULL pointers, and I am not getting any major errors or warnings out of gcc, so I am a bit stumped.

Anything out of the ordinary, or is there a better way to tackle this?

When you say you are "overlaying" your data-structure on the packet (at least that's what it seems you're saying), the value that appears in the four or eight bytes of your void* pointer data-member (depending on your platform) is most likely a value inside either a sctp_init or scpt_data chunk, not an actual pointer to that chunk of data. That is most likely why your pointer is not NULL, yet your seg-faulting when dereferencing the value in the pointer.

Secondly, overlaying a structure over serialized data without pragmas and/or compiler directives on the packing of the struture can be dangerous... sometimes how you think the compiler may allocate/pad the structure is not how it actually ends up doing it, and then the structure does not match the actual format of the data-packet, and you'll end up with invalid values.

So I'm assuming right now you're trying to-do something like this by directly overlaying the first N bytes of your packet with the scpt_header structure:

|scpt_header .........|void*|
|packet information ..|scpt_chunk.................|

That's not very portable, and can cause a lot of headaches especially when you insert network-byte-order issues into the situation. What you really want is something where you copy the contents of your SCPT packet into an internal data-structure, like a linked-list, that you can then properly manipulate.

One way could look like the following:

#include <arpa/inet.h>
unsigned char* packet_buffer = malloc(sizeof(PACKET_SIZE));

//... proceed to copy into the buffer however you are reading your packet

//now fill in your structure
unsigned char* temp = packet_buffer;
struct sctp_header header_data;

header_data.src_port = ntohs(*((uint16_t*)temp));
temp += sizeof(uint16_t);
header_data.dstport = ntohs(*((uint16_t*)temp));
temp += sizeof(uint16_t);
header_data.vtag = ntohl(*((uint32_t*)temp));
temp += sizeof(uint32_t);
//... keep going for all data members

//allocate memory for the first chunk (we'll assume you checked and it's a data chunk)
header_data.chunks = malloc(sizeof(sctp_data));
scpt_data* temp_chunk_ptr = header_data.chunks;

//copy the rest of the packet chunks into your linked-list data-structure
while (temp < (packet_buffer + PACKET_SIZE))
{
    temp_chunk_ptr->tsn = ntohl(*((uint32_t*)temp));
    temp += sizeof(uint32_t);
    //... keep going for the rest of this data chunk

    //allocate memory in your linked list for the next data-chunk
    temp_chunk_ptr->data = malloc(sizeof(scpt_data));
    temp_chunk_ptr = temp_chunk_ptr->data;
}

//free the packet buffer when you're done since you now have copied the data into a linked
//list data-structure in memory
free(packet_buffer);

This approach may seem cumbersome, but unfortunately if you're going to be dealing with endian and network-byte-order issues, you're not going to be able to simply overlay a data-structure on the packet data in a platform-portable manner, even if you declare it as a packed data-structure. Declaring a packed data-structure will align the bytes properly, but it won't correct endian issues which is what functions like ntohs and ntohl will do.

Also don't let your scpt_header go out-of-scope without destroying the linked list it "owns" or else you're going to end up with a memory leak.

Update : If you still want to go the overlaying route, first make sure you are using a compiler directive to pack your struct so that there is no padding added. In gcc this would be

typdef struct sctp_header {
    uint16_t srcport;
    uint16_t dstport;
    uint32_t vtag;
    uint32_t chksum;
} __attribute__((packed)) sctp_header;

typedef struct sctp_data 
{
    uint8_t ctype;
    uint8_t cflags;
    uint16_t clength;
    uint32_t tsn;
    uint16_t stream_id;
    uint16_t stream_seq;
    uint32_t pp_id;
} __attribute__((packed)) sctp_data;

typedef struct sctp_init 
{
    uint8_t ctype;
    uint8_t cflags;
    uint16_t clength;
    uint32_t initate_tag;
    uint32_t a_rwnd;
    uint16_t num_out_streams;
    uint16_t num_in_streams;
    uint32_t tsn;
} __attribute__((packed)) sctp_init;

but would be something else in other compilers. Also please note that I've changed your structures around a bit to better reflect how they are actually represented by the SCTP packet in-memory. Because the size of the two different packet types is different, we can't really do a union and overlay that in memory... the union will technically be the size of the largest chunk-type member, and that's going to create problems when we try to create arrays, etc. I'm also getting rid of the pointers... I see what you're trying to-do with them, but again, since you want to overlay these data-structures on the packet data, that's again going to cause problems since you're actually trying to "shift" the data around. The modifications to your original data-structures has been made to actually mirror how the data is laid out in memory without any fancy shifting or pointer casting. Since the type of each packet is represented by an unsigned char , we can now do the following:

enum chunk_type { DATA = 0, INIT = 1 };

unsigned char* packet_buffer = malloc(sizeof(PACKET_SIZE));
//... copy the packet into your buffer

unsigned char* temp = packet_buffer;

sctp_header* header_ptr = temp;
temp += sizeof(sctp_header);

//... do something with your header

//now read the rest of the packets
while (temp < (packet_buffer + PACKET_SIZE))
{
    switch(*temp)
    {
        case DATA:
            sctp_data* data_ptr = temp;
            //... do something with the data
            temp += data_ptr->clength;
            break;

        case INIT:
            sctp_init* init_ptr = temp;
            // ... do something with your init type
            temp += init_ptr->clength;
            break;

        default:
            //do some error correction here
    }
}

Keep in mind again that this method only corrects for alignment issues... it does not correct for endianness, so beware as you read any values in a multi-byte data-type.

The problem is that you shouldn't be using pointers at all. sctp_chunks should be:

union sctp_chunks {
    struct sctp_data data;
    struct sctp_init init;
};

And chunks in sctp_header should be union sctp_chunks chunks[1] (you can safely index chunks beyond 1 if you know the data will be valid.

I just saw Jason's answer, and he's right about structure packing. If you're using gcc, define the structure like:

struct sctp_header __attribute__ ((packed)) {
  ...
};

You'll have to do that for each structure.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM