Search code examples
ccastingconstantsundefined-behaviorconst-correctness

Is it legal/safe to cast away `const` for a heap-allocated object?


My use case is as follows.

I develop a library in which some loaded plugins can create objects (allocated using malloc() by the library), and some other plugins can read properties of those objects but not modify them.

For me this is a case of having a non-const API for the creating/writer side and a const API for the reader side, for example:

// writer API
struct obj *obj_create(void);
void obj_set_some_property(struct obj *obj, int property);

// reader API
int obj_get_some_property(const struct obj *obj);

The library casts a struct obj * (created by the writer side) to a const struct obj * (available to the reader side).

My problem is that those objects also have reference counts, and the reader side can call your typical reference count incrementation/decrementation functions. Those functions need to modify the object.

My question is: in this specific context, is it safe for the reference count incrementation/decrementation functions to accept a const struct obj * and cast away the const internally? Note that the reference count decrementation function could also destroy (free) the object if the count reaches zero.

I know that §6.7.3¶5 says:

If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined.

I just can't figure out what defined with a const-qualified type means. Does this apply if my object is heap-allocated? I totally understand why it would be UB to do so with a literal string pointer (.rodata), for example. But what if the object is created to be non-const in the beginning?

strchr() is a well known example of casting const away: it accepts const char * and returns char *, which points within the const char * parameter. How is this legal considering §6.7.3¶5?


Solution

  • In this context, defined refers to the program statement defining the variable, such as const struct obj x = {};. This is as opposed to a statement merely declaring it, such as const struct obj* x;.

    In C, memory returned by malloc is uninitialized storage that can be safely written to. In fact, your library must have done this at least once before passing the structure to the client!

    In theory, you might have a problem if the client somehow declared a const obj x = OBJ_INITIALIZER; and then passed it to your library. Your compiler might have stuck that variable definition into a read-only page of memory, or might optimize too aggressively on the assumption that it can never be modified. So you would need to specify that any library function that casts away const internally works only on objects from its own factories.

    One way to ensure that untrusted client code can’t violate this assumption is to pass in handles rather than object pointers, but your library might not need to bother.

    You might also conceivably run into problems if the client code uses the fields you modify, and if the compiler assumes that a function that takes a const struct obj* parameter cannot modify it through that parameter. You might work around that by returning a reference that aliases the object, which the compiler will not assume is the same unmodified object (or, I guess, by abusing volatile).

    In C++, you would have the option of declaring the fields that need to be modified mutable.