Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Towards strictly compliant usage of container_of

The container_of and its WinApi equivalent CONTAINING_RECORD are popular and useful macros. In principle, they use pointer arithmetic over char* to recover a pointer to an aggregate to which a given pointer to the member belongs.

The minimalistic implementation is usually:

#define container_of(ptr, type, member) \
   (type*)((char*)(ptr) - offsetof(type, member))

However, the strict compliance of a usage pattern of this macro is debatable. For example:

struct S {
    int a;
    int b;
};

int foo(void) {
    struct S s = { .a = 42 };
    int *p = &s.b;
    struct S *q = container_of(p, struct S, b);
    return q->a;
}

To my understanding, the program is not strictly compliant because:

  • expression s.b is an l-value of type int
  • &s.b is a pointer. Its value may carry implementation defined attributes like a size of a value it is pointing to
  • (char*)&s.b does not do anything special to the potential metadata bound to the value of the pointer
  • (char*)&s.b - offsetof(struct S, b), here UB is invoked because of pointer arithmetic outside of the value that the pointer is pointing to

I've noticed that the problem is not the container_of macro itself. It is rather the way how ptr argument is constructed. If the pointer was computed from the l-value of struct S type then there would be no out-of-bounds arithmetic. There would be no UB. A potentially compliant version of the program would be:

int foo(void) {
    struct S s = { .a = 42 };
    int *p = (int*)((char*)&s + offsetof(struct S, b));
    struct S *q = container_of(p, struct S, b);
    return q->a;
}

The actual arithmetic taking place is:

container_of(ptr, struct S, b)

Expand container_of

(struct S*)((char*)(ptr) - offsetof(struct S, b))

Place expression for ptr

(struct S*)((char*)((int*)((char*)&s + offsetof(struct S, b))) - offsetof(struct S, b))

Drop casts (char*)(int*)

(struct S*)((char*)&s + offsetof(struct S, b) - offsetof(struct S, b)))

Adding offsetof(struct S,b) does not overflow struct S. There is no UB when doing arithmetics. The positive and negative terms are reduced.

(struct S*)((char*)&s)

Now drop redundant casts.

&s

The question.

Is the above derivation correct?

Is such a usage of container_of strictly compliant?

If so, then the computation of a pointer to the member could be delegated to a new macro named member_of. The pointer can be constructed in a similar fashion as container_of. This new macro would be a complement of container_of to be used in strictly compliant programs.

#define member_of(ptr, type, member) \
   (void*)((char*)(ptr) + offsetof(type, member))

or a bit more convenient and typesafe but less portable (though fine in C23) version:

#define member_of(ptr, member) \
   (typeof(&(ptr)->member))((char*)(ptr) + offsetof(typeof(*(ptr)), member))

The program would be:

int foo(void) {
    struct S s = { .a = 42 };
    int *p = member_of(&s, struct S, b);
    struct S *q = container_of(p, struct S, b);
    return q->a;
}
like image 1000
tstanisl Avatar asked Aug 31 '25 11:08

tstanisl


1 Answers

&s.b is a pointer. Its value that may carry implementation defined attributes like the size of a value it is pointing to

There are two cases of pointer metadata

Type #1 - Pointers point to allocated buffers where a hidden preamble holds metadata for the allocated block.

From this slideshow (slide #9 onwards):

enter image description here

This definitely doesn't affect pointer arithmetic and was not the case OP was referring to.

Type #2 - Provenance or other metadata embedded into the pointer

Here's the draft for "A Provenance-aware Memory Object Model for C". It describes the idea behind implementing pointer resolution provenance in C.

There's a quote discussing member offsets:

Pointer member offset Given a non-null pointer p at C type τ , which points to the start of a struct or union type object (ISO C suggests this has to exist, writing “The value is that of the named member of the object to which the first expression points”) with a member m, if p is (π, a), the result of offsetting the pointer to member m has the same provenance π and the suitably offset a.

Combined with two later statements about pointer arithmetic:

Pointer addition and subtraction Pointer arithmetic (addition or subtraction of integers) preserves provenance. The resulting pointer value is indeterminate if the result not within (or one-past) the storage instance.

Pointer difference Pointer difference is only defined for pointers with the same provenance and within the same array...

And the fact there are no proposals to change section 6.2.5 in the ISO standard that discusses pointer arithmetic.

Leads to the only possible conclusion, which is this is ok.


A different question would be whether or not the (char*)(ptr) operation violates strict aliasing rules.

Strict aliasing definition (just in case), from a different stack overflow post:

"Strict aliasing is an assumption, made by the C (or C++) compiler, that dereferencing pointers to objects of different types will never refer to the same memory location (i.e. alias each other.)"

But because the operation is within the same struct and we only use it for compile-time calculations, this is ok.

like image 71
Daniel Trugman Avatar answered Sep 03 '25 02:09

Daniel Trugman