
The warning
During one of my presentations at Open Source Summit Japan1 🇯🇵 the past year, I talked about a bug I found while addressing -Wflex-array-member-not-at-end issues in the Linux kernel. The warning reported by the compiler was the following.
drivers/net/virtio_net.c:429:46: warning: structure containing a flexible array member is not at the end of another structure [-Wflex-array-member-not-at-end]
See the related code below.
392 struct virtnet_info {
...
429 struct virtio_net_rss_config_trailer rss_trailer; /* <- -Wfamnae warning */
430 u8 rss_hash_key_data[VIRTIO_NET_RSS_MAX_KEY_SIZE];
431
...
496 };
According to the compiler, struct virtio_net_rss_config_trailer at line 429 above is a flexible structure; this is a structure that contains a flexible-array member (FAM), and the issue is that the FAM in this structure is not at the end of struct virtnet_info.
Let’s remember that not-at-end FAMs are a compiler extension that may cause undefined behavior, and compilers do not handle the sizes of objects containing them consistently. For this reason, they are now deprecated:
struct flex {
int length;
char data[];
};
struct mid_flex {
int m;
struct flex flex_data;
int n;
};In the above, accessing a member of the array
mid_flex.flex_data.data[]might have undefined behavior. Compilers do not handle such a case consistently. Any code relying on this case should be modified to ensure that flexible array members only end up at the ends of structures.2
flexible-array members and tail padding
Now, let’s take a look at the internals of struct virtio_net_rss_config_trailer using the Poke a Hole (pahole) tool.
pahole -C virtio_net_rss_config_trailer drivers/net/virtio_net.o
struct virtio_net_rss_config_trailer {
__le16 max_tx_vq; /* 0 2 */
__u8 hash_key_length; /* 2 1 */
__u8 hash_key_data[]; /* 3 0 */
/* size: 4, cachelines: 1, members: 3 */
/* padding: 1 */
/* last cacheline: 4 bytes */
};
As we can see above, the FAM hash_key_data[] is located at offset 3 and, of course, its size is 0 bytes. However, the size of the structure is 4 bytes, which means that between the end of the struct and the address of the FAM there is 1 byte of padding. As shown above, this is also displayed by pahole: /* padding: 1 */.
This type of padding between the last member and the end of a structure is called tail padding, and it’s quite common. However, it can be problematic if people are not familiar with the nuances of flexible-array members. This is one of those subtle details developers should be acutely aware of when working with FAMs, in particular.
Based on the work over the past few years making flexible-array members safer3 and addressing -Wflex-array-member-not-at-end issues across the whole kernel tree, I’ve come to notice that it’s not uncommon for people to assume that the offset of a FAM is always at the very end of the flexible structure. That is, people tend to assume that offsetof(struct flex, fam) == sizeof(struct flex) is always true. However, due to tail padding, this is not always the case and, as we’re about to see, it should not be taken for granted.
So, what we have here is the following.
struct virtio_net_rss_config_trailer {
__le16 max_tx_vq; /* 0 2 */
__u8 hash_key_length; /* 2 1 */
__u8 hash_key_data[]; /* 3 0 */
/* size: 4, cachelines: 1, members: 3 */
/* padding: 1 */
/* last cacheline: 4 bytes */
};
offsetof(struct virtio_net_rss_config_trailer, hash_key_data) == 3
sizeof(struct virtio_net_rss_config_trailer) == 4
offsetof(struct virtio_net_rss_config_trailer, hash_key_data) != sizeof(struct virtio_net_rss_config_trailer)
Key takeaway: offsetof(struct flex, fam) != sizeof(struct flex) when tail padding is present.
The bug
Okay, now let’s take a closer look at struct virtnet_info with the help of pahole. Note how the next member after the flexible structure is a fixed-size array: u8 rss_hash_key_data[40].
pahole -C virtnet_info drivers/net/virtio_net.ostruct virtnet_info {
...
struct virtio_net_rss_config_trailer rss_trailer; /* 80 4 */
/* XXX last struct has 1 byte of padding */
u8 rss_hash_key_data[40]; /* 84 40 */
...
};
Aside from the tail padding in struct virtio_net_rss_config_trailer, two more things caught my attention here:
- a) The name of the fixed-size array:
rss_hash_key_data,is suspiciously similar to the FAM’s name:rss_trailer.hash_key_data[]. - b) The type of the fixed-size array is basically the same as the element type of the FAM.
These two things tell me that, probably, the original intention here was to overlap both the FAM and the fixed-size array; what could also be thought of as an implicit union between the two arrays.
So we effectively have an implicit union between rss_trailer.hash_key_data[] and rss_hash_key_data[VIRTIO_NET_RSS_MAX_KEY_SIZE]. However, due to the tail padding in struct virtio_net_rss_config_trailer previously described, these arrays do not actually share the same address in memory: hash_key_data being at offset 83, and rss_hash_key_data at offset 84 in struct virtnet_info. See below.
struct virtio_net_rss_config_trailer {
__le16 max_tx_vq; /* 0 2 */
__u8 hash_key_length; /* 2 1 */
__u8 hash_key_data[]; /* 3 0 */
/* size: 4, cachelines: 1, members: 3 */
/* padding: 1 */
/* last cacheline: 4 bytes */
};
struct virtnet_info {
...
struct virtio_net_rss_config_trailer rss_trailer; /* 80 4 */
/* XXX last struct has 1 byte of padding */
u8 rss_hash_key_data[40]; /* 84 40 */
...
};
offsetof(struct virtnet_info, rss_trailer.hash_key_data) == 83
offsetof(struct virtnet_info, rss_hash_key_data) == 84
Therefore, this results in a misalignment bug that must be fixed. 🐛
flexible-array members and implicit unions
Before getting into the details of how to properly fix this issue, let me note that implicit unions between a FAM and a fixed-size array are not uncommon in kernel code. I’ve addressed several -Wflex-array-member-not-at-end warnings when this was the case before (however, those did not manifest any misalignment bug). This is actually one of the categories of not-at-end FAMs I often discuss at conferences:

What’s crucial for implicit unions of this kind is that the arrays are correctly aligned. That is, both arrays must share the same address in memory. We also need to ensure that this alignment is not inadvertently changed in the future.
The bugfix
So, going back to the bug in question, we need to address two issues:
- 1) We need to find a way to make the FAM in
struct virtio_net_rss_config_trailerappear at the end ofstruct virtnet_info. With this, we’ll be fixing the-Wflex-array-member-not-at-endwarning. - 2) We need to fix the misalignment bug between the FAM and the fixed-size array.
Over the past couple of years we, in the Linux Kernel Self-Protection Project5, have implemented different methods and strategies to address these FAM not-at-end issues. The most recent innovation we’ve come up with for this is the TRAILING_OVERLAP()6 helper macro.
The idea behind this helper is to address the common pattern of implicit unions between a FAM and a number of following adjacent members of any type in a structure by creating an explicit union between them. I plan to talk more in detail about this macro, and show different scenarios where it can be used in a future post. For now, I’ll just show the bugfix7 below and describe the fixed up memory layout.
diff --git a/drivers/net/virtio_net.c b/drivers/net/virtio_net.c
index ca92b4a1879ce..db88dcaefb20b 100644
--- a/drivers/net/virtio_net.c
+++ b/drivers/net/virtio_net.c
@@ -425,9 +425,6 @@ struct virtnet_info {
u16 rss_indir_table_size;
u32 rss_hash_types_supported;
u32 rss_hash_types_saved;
- struct virtio_net_rss_config_hdr *rss_hdr;
- struct virtio_net_rss_config_trailer rss_trailer;
- u8 rss_hash_key_data[VIRTIO_NET_RSS_MAX_KEY_SIZE];
/* Has control virtqueue */
bool has_cvq;
@@ -484,7 +481,16 @@ struct virtnet_info {
struct failover *failover;
u64 device_stats_cap;
+
+ struct virtio_net_rss_config_hdr *rss_hdr;
+
+ /* Must be last as it ends in a flexible-array member. */
+ TRAILING_OVERLAP(struct virtio_net_rss_config_trailer, rss_trailer, hash_key_data,
+ u8 rss_hash_key_data[VIRTIO_NET_RSS_MAX_KEY_SIZE];
+ );
};
+static_assert(offsetof(struct virtnet_info, rss_trailer.hash_key_data) ==
+ offsetof(struct virtnet_info, rss_hash_key_data));
struct padded_vnet_hdr {
struct virtio_net_hdr_v1_hash hdr;
After using TRAILING_OVERLAP(), and moving both struct virtio_net_rss_config_trailer rss_trailer; and u8 rss_hash_key_data[VIRTIO_NET_RSS_MAX_KEY_SIZE]; at the end of struct virtnet_info, we end up with the following memory layout.
After changes, those members are correctly aligned at offset 795:
struct virtnet_info {
...
union {
struct virtio_net_rss_config_trailer rss_trailer; /* 792 4 */
struct {
unsigned char __offset_to_hash_key_data[3]; /* 792 3 */
u8 rss_hash_key_data[40]; /* 795 40 */
}; /* 792 43 */
}; /* 792 44 */
...
};
As mentioned above, rss_trailer.hash_key_data and rss_hash_key_data are now correctly aligned at offset 795 (the 1-byte padding in between is gone), and FAM rss_trailer.hash_key_data is seen by the compiler as being at the end of struct virtnet_info. With this, both the warning and the bug are effectively resolved.
Lastly, the static_assert() line ensures that the alignment does not inadvertently change.
Conclusion
To conclude, let me just note that this is yet another subtle bug that -Wflex-array-member-not-at-end has helped us uncover in the Linux kernel. As I shared in a previous post8, we’ve recently completed 85% of the work toward enabling this compiler option in mainline. Once it is globally enabled, it’s unlikely that these and other classes of memory corruption bugs9 10 11 12 will make their way into the upstream Linux kernel going forward.
Thanks! 🙂🐧🛡⚔️
- Upstream Kernel Hardening: Progress on enabling -Wflex-array-member-not-at-end ↩︎
- https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html ↩︎
- Safer flexible arrays for the kernel ↩︎
- Upstream Kernel Hardening: Progress on enabling -Wflex-array-member-not-at-end (OSSJP2025) ↩︎
- Linux Kernel Self-Protection Project ↩︎
- stddef: Introduce TRAILING_OVERLAP() helper macro ↩︎
- virtio_net: Fix misalignment bug in struct virtnet_info ↩︎
- 100 -Wflex-array-member-not-at-end issues in linux-next ↩︎
- qed/red_ll2: Fix undefined behavior bug in struct qed_ll2_info ↩︎
- wifi: mac80211: ieee80211_i: Fix memory corruption bug in struct ieee80211_chanctx ↩︎
- clk: clk-loongson2: Fix memory corruption bug in struct loongson2_clk_provider ↩︎
- cgroup: Eliminate cgrp_ancestor_storage in cgroup_root ↩︎