Friday, August 11, 2023

Interprocess Communication

In C

Let's review inter-process communication. IPC is, of course, how software sometimes passes information to other components, as well as to divy out access to restricted resources. This can be quite convoluted and complex in some cases. But here we'll review how this works in C. In C, we can open up pipes and pass information around to them using file descriptors.

pipe() creates a pipe, a unidirectional data channel that can be used for interprocess communication. The array pipefd is used to return two file descriptors referring to the ends of the pipe. pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of the pipe. Data written to the write end of the pipe is buffered by the kernel until it is read from the read end of the pipe.[1]

In our main function, we define a pipe file descriptor and pid_t value, which we will then use when we fork a child of our existing process. Of course, we check defensively check for error conditions first and fail out if they occur.

After the fork, the parent process continues on, performing the formality of closing the read pipe, before sending a message to the child process via the appropriate file descriptor, pipe_fd[0]. The parent process then closes it's write related file descriptor to signal that it has nothing left to write, then waits on the child process.

The child process then closes it's write related file descriptor, pipe_fd[1], before doing anything, as we're reading, not writing, bytes. We generate a 1024 size buffer and read the contents of pipe_fd[0] into it, and terminate the read with a null byte. Then we close the read related file descriptor and exit successfully.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int pipe_fd[2];
    pid_t child_pid;

    if (pipe(pipe_fd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    child_pid = fork();

    if (child_pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (child_pid == 0) { 
        close(pipe_fd[1]); 

        char buffer[1024];
        ssize_t bytes_read = read(pipe_fd[0], buffer, sizeof(buffer));
        buffer[bytes_read] = '\0';

        printf("Child received message: %s\n", buffer);

        close(pipe_fd[0]);
        exit(EXIT_SUCCESS);
    } else { // Parent process
        close(pipe_fd[0]); 

        char message[] = "Psst .. hello from parent process!";
        printf("Parent process message: %s\n", message);
        write(pipe_fd[1], message, strlen(message));

        close(pipe_fd[1]);
        wait(NULL); 
    }

    return 0;
}

After compiling, we can execute and observe the outputs are correctly printed to the terminal, like so. This is to say, our program forks a secondary process. And our parent process writes to a pipe, which the child process then reads:

hexagr@am c % ./ipc    
Parent process message: Psst .. hello from parent process!
Child received message: Psst .. hello from parent process!

We can further observe this in gdb using set follow-fork-mode child:

(gdb) continue
Continuing.
[Attaching after Thread 0x7ffff7d85740 (LWP 1379) fork to child process 1380]
[New inferior 3 (process 1380)]
[Detaching after fork from parent process 1379]
[Inferior 2 (process 1379) detached]
Parent process message: Psst .. hello from parent process!
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[Switching to Thread 0x7ffff7d85740 (LWP 1380)]
(gdb) continue
Continuing.
Child received message: Psst .. hello from parent process!

In JavaScript

A somewhat similar idiom is utilized in web frameworks like Chromium, Electron, and web technologies. For example, the browser Chrome uses a service called Mojo to do inter-process communication. But here, we'll use a similar technology, Node, to demonstrate IPC in the context of a web app. In this context, IPC is often used to pass information between remote and local processes, or perhaps from one software framework to another. First we'll build out our renderer.js prototype interface. Here we define our send and receive buttons:

const sendButton = document.getElementById('sendButton');
const receivedButton = document.getElementById('receivedButton');
const messageToSend = document.getElementById('messageToSend');

sendButton.addEventListener('click', async () => {
  const response = await ipcRenderer.invoke('message-from-renderer', 
messageToSend.textContent);
  receivedButton.removeAttribute('disabled');
  receivedButton.textContent = response;
});

receivedButton.addEventListener('click', () => {
  console.log('Received message:', receivedButton.textContent);
});

Next we'll use contextBridge in our preload.js file, to expose the API to our renderer:

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('ipcRenderer', ipcRenderer);

In main.js, we'll create a simple Window, setup a load for our future index.html, along with our IPC function, and activate the browser window:

const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
    },
  });

  win.loadFile('index.html');
}

app.whenReady().then(() => {
  createWindow();

  ipcMain.handle('message-from-renderer', (event, message) => {
    const response = `Received: ${message}`;
    return response;
  });
});


app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});

Last, a short index.html to define our buttons.

<!DOCTYPE html>
<html>
<head>
  <title>IPC</title>
</head>
<body>
  <button id="sendButton">Send Message</button>
  <button id="receivedButton" disabled>Received: Click to Display</button>
  <p id="messageToSend">Hola, mundo!</p>

  <script src="renderer.js"></script>
</body>
</html></code></pre>
IPC demonstration

After triggering the send function, our message is read by the receiving pipe and our receive button is enabled.

A use-after-free in an IPC component

While trawling Chromium's bug tracker recently, I stumbled over a very nice example of a Chromium use-after-free that was patched in 2014. The use-after-free occurs in an IPC component to help parse websockets. The problem here isn't inter-process communication in and of itself. Rather that bugs can creep in as software stacks expand and become more complex. Here we have the function for handling websockets prior to the fix:

WebSocketHostState WebSocketDispatcherHost::SendOrDrop(IPC::Message* message) {
  if (!Send(message)) {
    DVLOG(1) << "Sending of message type " << message->type()
             << " failed. Dropping channel.";
    DeleteWebSocketHost(message->routing_id());
    return WEBSOCKET_HOST_DELETED;
  }
  return WEBSOCKET_HOST_ALIVE;
}

The issue in the above function is that WebSocketHostState is handling a pointer to a message value. But the function is checking to see if the Send(message) is true or not.

And even if Send(message) returns false or fails - the message values are still utilized and accessed as is. So, even if the message values are corrupted, inside DVLOG, there are direct accesses to both message->type() and message->routing_id().

Here's the same function after the patch:

WebSocketHostState WebSocketDispatcherHost::SendOrDrop(IPC::Message* message) {
  const uint32 message_type = message->type();
  const int32 message_routing_id = message->routing_id();
  if (!Send(message)) {
    message = NULL;
    DVLOG(1) << "Sending of message type " << message_type
             << " failed. Dropping channel.";
    DeleteWebSocketHost(message_routing_id);
    return WEBSOCKET_HOST_DELETED;
  }
  return WEBSOCKET_HOST_ALIVE;
}

The fix is pretty straightforward — message->type() and message->routing_id() are now both properly defined in the function such that, before DVLOG, if the Send(message) value has brought things to a failed state, the message value is properly nulled out before the values are accessed.

No comments:

Post a Comment