Re: [RFC PATCH v2 3/4] liveupdate: Replace compatibility string with a 2-bit pair feature header
From: David Matlack
Date: Thu Oct 22 2026 - 13:51:57 EST
On 2026-10-22 12:11 AM, Pasha Tatashin wrote:
> On Wed, Oct 21, 2026 at 11:53 PM David Matlack <dmatlack@xxxxxxxxxx> wrote:
> > > #define DECLARE_LUO_SER(name, expected_size, ext_flags_count, payload_type) \
> > > struct name { \
> > > __u32 size; \
> > > __u32 flags; \
> > > __u64 ext_flags[ext_flags_count]; \
> > > payload_type payload; \
> > > }; \
> > > static_assert(sizeof(struct name) == (expected_size), \
> > > "LUO struct " #name " size mismatch!")
> > >
> > > For example:
> > >
> > > struct luo_memfd_payload {
> > > __u64 pos;
> > > __u64 size;
> > > };
> > >
> > > DECLARE_LUO_SER(luo_memfd_ser, 24, 0, struct luo_memfd_payload);
> >
> > This looks pretty reasonable but I would name the payload
> > luo_memfd_ser. The combined struct that DECLARE_LUO_SER() creates
> > should never be used directly since the offset of the payload in that
> > struct is only valid in the current kernel. So I would give that
> > struct a special name that makes it obvious you shouldn't be using it.
>
> Lets drop the fourth macro argument and generate the combined struct name:
>
> #define DECLARE_LUO_SER(name, expected_size, ext_flags_count) \
> struct name##_hdr { \
> __u32 size; \
> __u32 flags; \
> __u64 ext_flags[ext_flags_count]; \
> struct name payload; \
> }; \
> static_assert(sizeof(struct name##_hdr) == (expected_size), \
> "LUO struct " #name " size mismatch!")
>
> For example:
>
> struct luo_memfd_ser {
> __u64 pos;
> __u64 size;
> };
>
> DECLARE_LUO_SER_HDR(luo_memfd_ser, 24, 0);
I think we are getting close, bu I have two remaining concerns:
* Reserving bits in each flags word for continuation leaves gaps
in the flag space that users have to know to skip.
* struct name##_hdr only matches structs written by the current kernel
(in terms of length of array and offset of payload).
How about this instead:
struct luo_ser_hdr {
__u32 size; /* Size of header + payload in bytes */
__u32 nr_flags; /* Number of features encoded in flags[] */
__u64 flags[];
};
Each feature occupies 2 bits of flags[], so feature N is always bits
[2N+1:2N] of flags[N / 32], with no gaps:
00: Feature not present
01: Feature present, safe to ignore if not supported
11: Feature present, must be supported by the next kernel
10: Invalid
The length of flags[] and the header size are computed directly rather
than by walking continuation bits:
static inline u32 luo_ser_nr_flag_words(const struct luo_ser_hdr *hdr)
{
return DIV_ROUND_UP(hdr->nr_flags, 32);
}
static inline size_t luo_ser_hdr_size(const struct luo_ser_hdr *hdr)
{
return struct_size(hdr, flags, luo_ser_nr_flag_words(hdr));
}
static inline void *luo_ser_payload(const struct luo_ser_hdr *hdr)
{
return (void *)hdr + luo_ser_hdr_size(hdr);
}
and bounds checking is just luo_ser_hdr_size(hdr) <= hdr->size. The
payload is always 8-byte aligned.
Payload structs are then declared on their own, with no header:
struct luo_memfd_ser {
__u64 pos;
__u64 size;
};
LUO_SER_ASSERT_SIZE(luo_memfd_ser, 16);
The static_assert covers the payload to catch embedded structs changing
size. Asserting the header doesn't add anything since its size varies
by kernel. And because flags[] is a flexible array, the header can't be
embedded in front of a payload in a combined struct; it can only be
reached through luo_ser_payload().
I think the only downside is that the header increases to 16-bytes when
there is at least 1 flag. But I don't think we need to be so concerned
about an 8 byte difference...