Skip to content

Make management of C-memory allocated from OCaml more explicit #571

Description

@robhoes

Ctypes has a number of functions that allocate C memory and return an OCaml value containing a pointer to it. Examples are make and CArray.make. This C memory is automatically freed when the associated OCaml value is collected by the GC due to the finaliser that is attached to it.

This is convenient, since it makes it harder for memory leaks to occur due to forgetting to free the memory. On the other hand, to avoid memory corruption, you need to be very careful to ensure that the OCaml value is kept alive (not GC-able) for as long as there is (C) code using the memory. It turns out that it is not always straightforward to do this, and it is easy to make mistakes.

An example is the use of a C function that takes a pointer to memory allocated from OCaml (as above), and returns a pointer to (somewhere in) the memory, perhaps after writing some data to it. The returned pointer will be wrapped in a new OCaml value, distinct from the original one, because ctypes can't tell that they are related. We must now keep the original value alive, along with the returned one.

A real example (thanks @yallop):

let strchr = Foreign.foreign "strchr" (ptr char @-> char @-> returning (ptr char)) in
let p = CArray.of_string "abc" in
let q =  strchr (CArray.start p) 'a' in
let () = Gc.compact () in
Printf.printf "%c\n" !@q

Immediately after strchr returns, value p is at risk of being GC'ed, which would free the array. From then on, q is pointing to invalid memory, and an arbitrary character may be printed in the last line. To avoid this, we would have to actually "use" p somehow after the last use of q, which is awkward and unintuitive, and therefore error prone.

See xapi-project/xen-api#3669 for a real-life example of a fix that was made for such a problem.

Something similar occurs when allocating memory from OCaml, and storing the pointer in a struct, even if the struct is also allocated from OCaml. It is mandatory to keep all OCaml values alive. See xapi-project/ocaml-opasswd#14 for an example of this, which is a fix that still turned out to be incomplete!

There are several other subtleties in the area of memory management, which I have not mentioned above, but which users of ctypes should be aware to avoid corruption. Such mistakes could be avoided by adopting a more explicit memory model, which does not use finalisers, and where it is up to the OCaml program to free the (C) memory using a call to a deallocation function (e.g. Ctypes.deallocate). The trade-off is indeed that doing so introduces a risk of memory leaks, but it may be clearer to see what is going on.

A possible way to do this, while maintaining backwards compatibility, is to add an optional, Boolean argument to all "make" functions, which has the effect that no finaliser is added to the new value.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions