The IPC namespace is used for isolating System V IPC objects, and POSIX message queues. The clone flag used to achieve this is CLONE_NEWIPC. We’ve adapted our program from previous articles, to create a POSIX message queue, which will be used to pass a message between a process and its cloned child process.

To demonstrate how the IPC namespace establishes isolation of a POSIX message queue, we’ll create a message queue in the parent process, and attempt to open it from within the child process, in order to send a message from the child to the parent process. We’ll do this once when the cloned child process remains in the global IPC namespace, and then when it is created within a new IPC namespace.

So, first off, we need to embellish the program, which is available here (invoke_ns5.c), with a new command line option, -i. The -i option must be accompanied with a string of either ‘yes’ or ‘no’. If ‘yes’ is specified, the child process will be cloned in a new IPC namespace, and if ‘no’ is specified, it remains in the global IPC namespace.

The command line parsing now looks like this:

// Parse command line options and construct arguments
// to be passed to childFunction
while ((option = getopt(argc, argv, "+hvpmu:ni:")) != -1) {
    switch (option) {
        case 'i':
            if (strcmp("no", optarg) != 0 && strcmp("yes", optarg) != 0) {
                fprintf(stderr, "%s: option requires valid argument -- 'i'\n", argv[0]);
                usage(argv[0]);
                exit(EXIT_FAILURE);
            }
            else
                if (strcmp("yes", optarg) == 0)
                    flags |= CLONE_NEWIPC;
            args.ipc = 1;
            break;
        case 'n':
            flags |= CLONE_NEWNET;
            break;
        case 'u':
            flags |= CLONE_NEWUTS;
            args.hostname = malloc(sizeof(char) * (strlen(optarg) + 1));
            strcpy(args.hostname, optarg);
            break;
        case 'm':
            flags |= CLONE_NEWNS;
            break;
        case 'p':
            flags |= CLONE_NEWPID;
            break;
        case 'v':
            args.verbose = 1;
            break;
        case 'h':
            usage(argv[0]);
            exit(EXIT_SUCCESS);
        default:
            usage(argv[0]);
            exit(EXIT_FAILURE);
    }
}

We also need to define the message queue name:

// Define message queue name
const char *mq_name = "/ipc_namespace";

If the -i command line option has been specified, before making the call to clone, the parent calls the prepareMQ function:

// Prepare message queue
if (args.ipc) {
    mq = prepareMQ(&args);
    if (mq == -1)
        exit(EXIT_FAILURE);
}

The prepareMQ function creates a message queue (in the global IPC namespace), and opens it for reading and writing, with a maximum message size of 81 bytes (an extra byte for termination):

// Prepare message queue
mqd_t prepareMQ(void *child_args)
{
    struct arguments *args = child_args;
    mqd_t            mq;
    int              oflags = O_CREAT | O_RDWR;
    struct mq_attr   attr;

    attr.mq_flags   = 0;
    attr.mq_maxmsg  = 10;
    attr.mq_msgsize = 81;
    attr.mq_curmsgs = 0;

    mq = mq_open(mq_name, oflags, 0644, &attr);
    if (mq != -1) {
        if (args->verbose)
            printf("Parent: opening message queue %s\n", mq_name);
    }
    else
        perror("Parent: prepareMQ: mq_open");

    return mq;
}

Once the message queue has been established, the child process is created with the clone system call, and then the parent waits 60 seconds for a message from the child process, before timing out. If a message is received from the child process, the parent process echoes this to stdout:

// Read message from child on message queue
if (args.ipc) {
    if (clock_gettime(CLOCK_REALTIME, &timeout) == 0) {
        timeout.tv_sec += 60;
        if (mq_getattr(mq, &attr) != -1) {
            msg = malloc(attr.mq_msgsize);
            if (mq_timedreceive(mq, msg, attr.mq_msgsize, NULL, &timeout) != -1) {
                if (args.verbose)
                    printf("Parent: received message from child\n");
                printf("\n    Parent: the following message was received from the child\n     >> %s\n\n", msg);
            }
            else
                perror("Parent: main: mq_timedreceive");
            free(msg);
        }
        else
            perror("Parent: main: mq_getattr");
    }
    else
        perror("Parent: main: clock_gettime");
}

Once the child process has terminated, the parent closes and removes the message queue:

// Remove message queue
if (args.ipc) {
    if (args.verbose)
        printf("Parent: closing message queue %s\n", mq_name);
    if (mq_close(mq) == -1)
        perror("Parent: main: mq_close");
    if (args.verbose)
        printf("Parent: removing message queue %s\n", mq_name);
    if (mq_unlink(mq_name) == -1)
        perror("Parent: main: mq_unlink");
}

With regard to the cloned child, we have added some additional code to the childFunction, which gets executed when the child process is created. If the -i command line option has been specified, the child attempts to open the message queue created by the parent, and prompts the user to enter a message, which is then sent to the message queue. If the child has got this far, it closes the message queue and continues to process the other namespace requirements:

// Send message to parent if -i option provided
if (args->ipc) {
    if (args->verbose)
        printf(" Child: opening message queue %s\n", mq_name);
    mq = mq_open(mq_name, oflags);
    if (mq != -1) {
        if (mq_getattr(mq, &attr) != -1) {
            msg = malloc(attr.mq_msgsize);
            printf("\n     Child: enter a message to send to the parent process (MAX 80 chars)\n     >> ");
            if (fgets(msg, attr.mq_msgsize, stdin) != NULL) {
                msg[strcspn(msg, "\n")] = '\0';
                printf("\n");
                if (args->verbose)
                    printf(" Child: sending message to parent\n");
                if (mq_send(mq, msg, attr.mq_msgsize,0) == -1)
                    perror(" Child: childFunction: mq_send");
            }
            else
                perror(" Child: childFunction: fgets");
            if (args->verbose)
                printf(" Child: closing message queue %s\n", mq_name);
            if (mq_close(mq) == -1)
                perror(" Child: childFunction: mq_close");
            free(msg);
        }
        else
            perror(" Child: childFunction: mq_getattr");
    }
    else
        perror(" Child: childFunction: mq_open");
}

Now let’s see what happens when we execute the program. First, we need to compile it, remembering to specify -lrt in order to link the POSIX real-time library into the executable, which contains the message queue API. In the first instance we’ll specify -i no, which means the cloned child process remains in the same IPC namespace as its parent, the global IPC namespace:

$ sudo ./invoke_ns -vi no
[sudo] password for wolf:
Parent: PID of parent is 11944
Parent: opening message queue /ipc_namespace
Parent: PID of child is 11945
 Child: PID of child is 11945
 Child: opening message queue /ipc_namespace

     Child: enter a message to send to the parent process (MAX 80 chars)
     >>

At this point, we need to type a message of up to 80 characters, perhaps ….

I will return to haunt you with peculiar piano riffs

After hitting return, the message is sent to the message queue, which the parent reads, and echoes to stdout. In this instance, because we have only specified the -i no option, the child process exits, which triggers the parent to close and remove the message queue, before it exits:

     Child: enter a message to send to the parent process (MAX 80 chars)
     >> I will return to haunt you with peculiar piano riffs

 Child: sending message to parent
 Child: closing message queue /ipc_namespace
Parent: received message from child

    Parent: the following message was received from the child
     >> I will return to haunt you with peculiar piano riffs

Parent: closing message queue /ipc_namespace
Parent: Removing message queue /ipc_namespace
Parent: ./invoke_ns - Finishing up

If we now run the program again, but this time specify -i yes as the command line option, the child process will be created in a new IPC namespace and will not be able to open the message queue, because it can’t see it:

$ sudo ./invoke_ns -vi yes
[sudo] password for wolf:
Parent: PID of parent is 12508
Parent: opening message queue /ipc_namespace
Parent: PID of child is 12509
 Child: PID of child is 12509
 Child: opening message queue /ipc_namespace
 Child: mq_open: No such file or directory

After 60 seconds, the parent’s wait for reading the message queue times out:

Parent: mq_timedreceive: Connection timed out
Parent: closing message queue /ipc_namespace
Parent: Removing message queue /ipc_namespace
Parent: ./invoke_ns - Finishing up

Each IPC namespace has its own message queue filesystem, which is only visible to processes residing in the same IPC namespace.

In the next article, we’ll look at cloning a process into several namespaces, along with a minimal chroot filesystem, in order to create a ‘rustic’ container.