Moving borrowed values into a forked child


I ran into some ownership issues working with Unix fork (through the nix crate). I wanted to move a value into the child but keep it borrowed in the parent.

I wrote this abstraction which I think is safe. I’d appreciate if someone could confirm the safety of it, or help me understand why it migh not be.

fn fork<T, C: FnOnce(T)/* -> ! */>(data_ref: &T, child: C) -> Pid {
    match nix::unistd::fork().unwrap() {
        ForkResult::Child => {
            let data = unsafe { (data_ref as *const T).read() };

        ForkResult::Parent { child } => child,

Note that the child closure shouldn’t return, but -> ! isn’t available yet so I just threw in unreachable!.

One point which I’m not sure about is read vs read_unaligned. I assumed that Rust references are always aligned.

It would look like

fork(&data, |data| {
    // do something with data

    // do something that doesn't return control flow
    // e.g. cmd.exec(), or a loop, or a panic

Regarding diverging closures (your -> ! requirement), see this internals thread: Set ‘static lifetime for local variables of diverging functions?:

enum Diverging {}

fn fork<T, F> (data_ref: &T, child: F) -> Pid
    F : FnOnce(&T) -> Diverging,
    match ::nix::unistd::fork().unwrap() {
        ForkResult::Child => {
            match child(&data) {} // : !

        ForkResult::Parent { child } => child,

But generally, using unix’s fork with shared pointers does not lead to the intuitive behavior you’d expect, and I am pretty sure it is not compatible with Rust guarantees: shared global memory (shm_* family, mmap) becomes inherently aliased and thus unsafe to mutate; the other memory is, AFAIK, entirely copied (copy-on-write for perfomance), so there is no real “sharing”, here. Finally, the child process can survive its parent’s death and thus outlive it, so careful with that too.


But aliasing is inherent to those APIs, right? I don’t see how forking makes it particularly unsafe.


Yep, hence mutation with those APIs is inherently racy, leading to potential UB unless it is synchronized.

The following C program, for instance, attempts to increment many times and in parallel from both the parent and the forked child a shared (through mmap) uint64_t, and leads to the given shared integer having around 52% of the total expected value:

C code
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define assert(condition) \
  if (!(condition)) { \
    fputs("Assertion failed: '" #condition "'.", stderr); \
    if (child != 0) { kill(child, SIGKILL); } \
    exit(EXIT_FAILURE); \

#define log(fmt, ...) fprintf(stderr, "[pid %ld] " fmt "\n", (long) getpid(), ##__VA_ARGS__)

static pid_t parent = 0;
static pid_t child = 0;

void wait_child_exit ()
    assert(child != 0);
    log("Waiting for child %ld to end...", (long) child);
    int status;
    waitpid(child, &status, 0);
    if (WIFEXITED(status)) {
        int exit_status = WEXITSTATUS(status);
        log("Child %ld exited with status %d", (long) child, exit_status);
        if (exit_status != EXIT_SUCCESS) {
    } else {
        log("Unexpected status.");
        kill(child, SIGKILL);

#define N 50000000

int main (void)
    size_t * shared_int;
    shared_int = mmap(NULL, sizeof(*shared_int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (shared_int == MAP_FAILED) {
    *shared_int = 0;
    parent = getpid();
    child = fork();
    switch (child) {
        // child
        case 0:
            child = getpid();
            for (size_t i = 0; (i++) < N; ++(*shared_int));

        // parent (fork failed)
        case -1:

        // parent (no error)
            for (size_t i = 0; (i++) < N; ++(*shared_int));
    printf("shared_int = %zu (expected %zu).\n", *shared_int, 2 * N);
    munmap(shared_int, sizeof(*shared_int));
    return EXIT_SUCCESS;
[pid 20881] Done.
[pid 20880] Done.
[pid 20880] Waiting for child 20881 to end...
[pid 20880] Child 20881 exited with status 0
shared_int = 52272791 (expected 100000000).
1 Like

You could use Infallible until ! is stabilized (it should even be no problem to use Infallible, because it will be a type alias soon.