[RFC PATCH v2 3/4] liveupdate: Replace compatibility string with a 2-bit pair feature header
Pasha Tatashin
pasha.tatashin at soleen.com
Thu Oct 22 13:53:28 PDT 2026
On Thu, Oct 22, 2026 at 1:51 PM David Matlack <dmatlack at google.com> wrote:
>
> On 2026-10-22 12:11 AM, Pasha Tatashin wrote:
> > On Wed, Oct 21, 2026 at 11:53 PM David Matlack <dmatlack at google.com> 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.
Yes, but callers should never manipulate the raw flag words directly,
and we can provide simple accessor macros or inlines that take a
contiguous feature ID (0, 1, 2, ...)
enum luo_memfd_feats {
LUO_MEMFD_FEAT_HUGETLB = 0,
LUO_MEMFD_FEAT_SEALS = 1,
/* ... */
LUO_MEMFD_FEAT_FOO = 15, /* Automatically lands in
ext_flags[0] bits 0:1 */
LUO_MEMFD_NR_FEATS
};
if (luo_ser_get_feat(hdr, LUO_MEMFD_FEAT_HUGETLB)) {
/* handle hugetlb */
}
> * struct name##_hdr only matches structs written by the current kernel
> (in terms of length of array and offset of payload).
This is not an issue on the incoming path, we never cast incoming data
to struct name##_hdr or access ->payload directly. Instead, we parse
the incoming buffer, dynamically compute the header size from the
continuation bits, and cast the payload offset to "struct name *"
based on the negotiated feature flags.
>
> 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...
I would prefer to keep the default header at 8 bytes. With 15 features
fitting directly inside the base __u32 flags word, adding the first 15
features to a struct keeps the header at 8 bytes and does not shift
the payload.
Computation-wise it is also very similar: for the 99% of structs that
have <= 15 features, LUO_FLAGS_CONT is 0, so computing the header size
is just a single bit test with no loop. Even if a struct ever needs
ext_flags, the loop will walk at most one or two extra words.
Pasha
More information about the kexec
mailing list