Standardized IPC protocol

Discussions on more advanced topics such as monolithic vs micro-kernels, transactional memory models, and paging vs segmentation should go here. Use this forum to expand and improve the wiki!
nexos
Member
Member
Posts: 1078
Joined: Tue Feb 18, 2020 3:29 pm
Libera.chat IRC: nexos

Re: Standardized IPC protocol

Post by nexos »

An APC is the Windows equivalent of signals. The kernel queues for an APC to fire, and when a thread returns to user mode, it checks this queue, and any APCs therein get fired.
"How did you do this?"
"It's very simple — you read the protocol and write the code." - Bill Joy
Projects: NexNix | libnex | nnpkg
User avatar
AndrewAPrice
Member
Member
Posts: 2299
Joined: Mon Jun 05, 2006 11:00 pm
Location: USA (and Australia)

Re: Standardized IPC protocol

Post by AndrewAPrice »

I finally have a working Permebuf IPC example! These are one way "mini-messages" (messages that are <=32 bytes and can be transfered in registers) between two services. The base classes (MouseDriver, MouseDriver::Server, MouseDriver::SetMouseListenerMessage, etc.) are all generated from a .permebuf file.

Basic mouse driver (I removed the actual hardware code, to highlight just the Permebuf service code):

Code: Select all

class PS2MouseDriver : public MouseDriver::Server {
public:
	PS2MouseDriver() :
		mouse_bytes_received_(0),
		last_button_state_{false, false, false} {}

	virtual ~PS2MouseDriver() {
		if (mouse_captor_) {
			// Tell the captor we had to let the mouse go.
			mouse_captor_->SendOnMouseRelease(MouseListener::OnMouseReleaseMessage());
		}
	}

	void HandleMouseInterrupt() {
		// ...
		ProcessMouseMessage(...);
	}

	virtual void HandleSetMouseListener(ProcessId,
		const MouseDriver::SetMouseListenerMessage& message) {
		if (!mouse_captor_) {
			// Let the old captor know the mouse has escaped.
			mouse_captor_->SendOnMouseRelease(MouseListener::OnMouseReleaseMessage());
		}
		if (message.HasNewListener()) {
			mouse_captor_ = std::make_unique<MouseListener>(message.GetNewListener());
			// Let our captor know they have taken the mouse captive.
			mouse_captor_->SendOnMouseTakenCaptive(
				MouseListener::OnMouseTakenCaptiveMessage());
		} else {
			mouse_captor_.reset();
		}
	}

private:
	// ...

	// The last known state of the mouse buttons.
	bool last_button_state_[3];

	// The service we should sent mouse events to.
	std::unique_ptr<MouseListener> mouse_captor_;

	// Processes the mouse message.
	void ProcessMouseMessage(uint8 status, uint8 offset_x, uint8 offset_y) {
		// Read if the mouse has moved.
		int16 delta_x = (int16)offset_x - (((int16)status << 4) & 0x100);
		int16 delta_y = -(int16)offset_y + (((int16)status << 3) & 0x100);

		// Read the left, middle, right buttons.
		bool buttons[3] = {
			(status & (1)) == 1,
			(status & (1 << 2)) == 4,
			(status & (1 << 1)) == 2};

		if ((delta_x != 0 || delta_y != 0) && mouse_captor_) {
			// Send our captor a message that the mouse has moved.
			MouseListener::OnMouseMoveMessage message;
			message.SetDeltaX(static_cast<float>(delta_x));
			message.SetDeltaY(static_cast<float>(delta_y));
			mouse_captor_->SendOnMouseMove(message);
		}

		for (int button_index : {0,1,2}) {
			if (buttons[button_index] != last_button_state_[button_index]) {
				last_button_state_[button_index] = buttons[button_index];
				if (mouse_captor_) {
					// Send our captor a message that a mouse button has changed state.
					MouseListener::OnMouseButtonMessage message;
					switch (button_index) {
						case 0: message.SetButton(MouseButton::Left); break;
						case 1: message.SetButton(MouseButton::Middle); break;
						case 2: message.SetButton(MouseButton::Right); break;
					}
					message.SetIsPressedDown(buttons[button_index]);
					mouse_captor_->SendOnMouseButton(message);
				}
			}
		}
	}
};

// Global instance of the mouse driver.
std::unique_ptr<PS2MouseDriver> mouse_driver;

void InterruptHandler() {
	mouse_driver->HandleMouseInterrupt();
}

int main() {
	mouse_driver = std::make_unique<PS2MouseDriver>();

	// ...

	// Listen to the interrupts.
	RegisterInterruptHandler(1, InterruptHandler);
	RegisterInterruptHandler(12, InterruptHandler);

	perception::TransferToEventLoop();
	return 0;
}
A program that that creates a MouseListener, implements just the methods it cares about (OnMouseMove, OnMouseButton), waits for an instance of the MouseDriver to appear (so it doesn't matter which order you start the two programs), and sends a reference to the listener class to another process.

Code: Select all

class MyMouseListener : public MouseListener::Server {
public:
	void HandleOnMouseMove(
		ProcessId, const MouseListener::OnMouseMoveMessage& message) override {
			std::cout << "X:" << message.GetDeltaX() <<
				" Y:" << message.GetDeltaY() << std::endl;
	}

	void HandleOnMouseButton(
		ProcessId, const MouseListener::OnMouseButtonMessage& message) override {
		std::cout << "Button: " << (int)message.GetButton() <<
			" Down: " << message.GetIsPressedDown() << std::endl;
	}
};


int main() {
	MyMouseListener mouse_listener;

	MessageId listener = MouseDriver::NotifyOnEachNewInstance(
		[&] (MouseDriver mouse_driver) {
			// Tell the mouse driver to send us mouse messages.
			MouseDriver::SetMouseListenerMessage message;
			message.SetNewListener(mouse_listener);
			mouse_driver.SendSetMouseListener(message); 

			// We only care about one instance. We can stop
			// listening now.
			MouseDriver::StopNotifyingOnEachNewInstance(
				listener);
	});
	perception::TransferToEventLoop();
	return 0;
}
Baby steps!
My OS is Perception.
User avatar
AndrewAPrice
Member
Member
Posts: 2299
Joined: Mon Jun 05, 2006 11:00 pm
Location: USA (and Australia)

Re: Standardized IPC protocol

Post by AndrewAPrice »

The next problem I'm going to think about is security. Only the window manager should be able to talk to the tell the mouse driver who to send messages directly to. We're a microkernel.

I'm thinking I'll need a Permissions Manager service, where I can send a permission (maybe as a string, e.g. "fs_read") + process ID, and it can reply "Yes" or "No" to the asking process, but before replying to the caller, the Permissions Manager could even prompt the user "Is 'Firefox' allowed to read from the file system?" But, who is the legitimate permissions manager? Some ideas I'm thinking:
  • The first "Permissions Manager" instance is the legit one. Essential system services need to be loaded in grub.cfg to ensure they are loaded first.
    • Pro: You can swap out the "Permissions Manager" and other essential services by changing your grub.cfg.
    • Con: If the first instance of "Permission Manager" dies, that means a second (and possibly compromised) Permission Manager takes over.
  • The kernel knows a name for each running process, so we only trust the "Permissions Manager" in "/applications/Permissions Manager.app".
    • Pro: If "Permissions Manager" dies, we can restart "/applications/Permissions Manager.app".
    • Con: But users can't fully customize their OS with their own services. Maybe they want to replace the device manager, or the window manager with one that is faster or has more features.
My OS is Perception.
Doctor5555
Posts: 14
Joined: Sat Oct 10, 2020 4:05 pm

Re: Standardized IPC protocol

Post by Doctor5555 »

AndrewAPrice wrote: But, who is the legitimate permissions manager?
One solution might be to allow the kernel to identify system services as "trusted" based on the origin, but allow that to be configured by the root/admin user. For example, by default the kernel only trusts "/applications/Permissions Manager.app" to correctly return permissions (and perhaps this is hard-coded as a default in case config files get corrupted), and can restart it, but root has permission to add new trusted/default permission managers in a config file.
andrew_w
Posts: 19
Joined: Wed May 07, 2008 5:06 am

Re: Standardized IPC protocol

Post by andrew_w »

AndrewAPrice wrote:The next problem I'm going to think about is security. Only the window manager should be able to talk to the tell the mouse driver who to send messages directly to. We're a microkernel.

I'm thinking I'll need a Permissions Manager service, where I can send a permission (maybe as a string, e.g. "fs_read") + process ID, and it can reply "Yes" or "No" to the asking process, but before replying to the caller, the Permissions Manager could even prompt the user "Is 'Firefox' allowed to read from the file system?" But, who is the legitimate permissions manager? Some ideas I'm thinking:
  • The first "Permissions Manager" instance is the legit one. Essential system services need to be loaded in grub.cfg to ensure they are loaded first.
    • Pro: You can swap out the "Permissions Manager" and other essential services by changing your grub.cfg.
    • Con: If the first instance of "Permission Manager" dies, that means a second (and possibly compromised) Permission Manager takes over.
  • The kernel knows a name for each running process, so we only trust the "Permissions Manager" in "/applications/Permissions Manager.app".
    • Pro: If "Permissions Manager" dies, we can restart "/applications/Permissions Manager.app".
    • Con: But users can't fully customize their OS with their own services. Maybe they want to replace the device manager, or the window manager with one that is faster or has more features.
I think it might be better to make your kernel capability oriented rather than having stateless IPC that uses process or thread IDs as destinations. That way, all the kernel has to do is track which capabilities a process has, and managing permissions is left up to whatever process(es) give out capabilities (e.g. a VFS), without any need for the kernel to ask a process on every message or memory mapping. Capabilities also have the advantage of decoupling IPC endpoints from the process/thread structure of the server, which is a good thing because it lets servers have multiple threads to respond to requests and it also makes it easier to restart crashed servers (PIDs/TIDs will change on restart in most systems, but a capability can be held open and given back to the server after it's restarted). It's probably also a good idea to have separate user-mode components for presenting a UI, persistence of permissions on disk, and actually checking the permissions on open.


Under UX/RT, the base permission manager will be part of the root server (i.e. the first process started by the microkernel), along with the base VFS and high-level memory management. It will have no concept of persistence at all (let alone a GUI), and will only track permissions for running processes, not programs on disk. There will be a separate permission store server to associate permissions with programs, roles, and users, started after the system volume is mounted (permissions for programs in the boot image will be determined entirely by the init system). There will be a generic mechanism in the VFS to allow programs to hook calls to exec, and the permission store server will use this to set the permissions for each process before it actually starts.

Since the seL4 kernel that I'm using is purely capability oriented and file descriptors and memory mappings will be mapped onto kernel capabilities, there will be no need for kernel hooks at all. What a process is allowed to access will be determined by a combination of its open capabilities and what new capabilities it is able to obtain through open() and other APIs that transfer capabilities. Permission checking will only be done on open(), since there will be no way to send messages or map memory without having open capabilities for it.

The UI components related to permissions will be separate from anything else and will just be front ends. Actually setting which permissions are associated with a program will usually be done by a CLI program that is normally invoked from the package manager, and there will also be a GUI to see what permissions a program has, but this will normally be read-only (installation outside the package manager for binary packages will be deprecated). There will be GUI and CLI utilities for escalating privileges as well.
Developer of UX/RT, a QNX/Plan 9-like OS
User avatar
AndrewAPrice
Member
Member
Posts: 2299
Joined: Mon Jun 05, 2006 11:00 pm
Location: USA (and Australia)

Re: Standardized IPC protocol

Post by AndrewAPrice »

andrew_w wrote:Permission checking will only be done on open(), since there will be no way to send messages or map memory without having open capabilities for it.
This is interesting because my Permebuf service protocol doesn't require you to open a channel to a service first. You can just send any RPC to any service, however the callee will know the PID of who's calling it. I'm not expecting a round-trip RPC to the Permissions Manager per call to check on permissions for EVERY RPC.

I will create a Permissions helper functions that caches the PID+Permission->bool locally:
  • PID 10 (Minesweeper) asks PID 3 (graphics driver) that it wants to go fullscreen
  • PID 3 (graphics driver) issues an RPC to Permissions Manager to ask if PID 10 (Minesweeper) has permission to "fullscreen" (and Permissions Manager could prompt the user - "Can 'Minesweeper' go full screen? [Always] [Just Once] [Never]" before replying to PID 3 (graphics driver).)
  • PID 3 (graphics driver) caches that PID 10 (Minesweeper) can "fullscreen", so for future requests from PID 10 (Minesweeper) to go fullscreen we don't need to issue an RPC to Permissions Manager.
  • Permissions Manager will remember that PID 50 (graphics driver) cares if PID 10 (Minesweeper) can "fullscreen", and will notify it to update its cache if this permission changes.
  • PID 3 (graphics driver) will ask the kernel to notify it when PID 10 (Minesweeper) dies so it can forget about PID 10 (Minesweeper) in its cache.
  • Permissions Manager will ask the kernel to notify it if either PID 3 (graphics driver) or PID 10 (Minesweeper) dies, so it can forget that PID 3 (graphics driver) cares about if PID 10 (Minesweeper) can "fullscreen".
andrew_w wrote:the base permission manager will be part of the root server (i.e. the first process started by the microkernel),
Perhaps I could make the Permissions Manager the first process - so it'll always be PID 1 (PID 0 is reserved for messages from the kernel.) As long as PID 1 isn't compromised, it can be the source of truth for the rest of the system.
My OS is Perception.
moonchild
Member
Member
Posts: 73
Joined: Wed Apr 01, 2020 4:59 pm
Libera.chat IRC: moon-child

Re: Standardized IPC protocol

Post by moonchild »

AndrewAPrice wrote:my Permebuf service protocol doesn't require you to open a channel to a service first. You can just send any RPC to any service, however the callee will know the PID of who's calling it. I'm not expecting a round-trip RPC to the Permissions Manager per call to check on permissions for EVERY RPC
A capability-based solution will likely be superior to this because it avoids the confused deputy problem. Access to any resource—including a communication channel—should be a privilege, not a right.
User avatar
AndrewAPrice
Member
Member
Posts: 2299
Joined: Mon Jun 05, 2006 11:00 pm
Location: USA (and Australia)

Re: Standardized IPC protocol

Post by AndrewAPrice »

moonchild wrote:
AndrewAPrice wrote:my Permebuf service protocol doesn't require you to open a channel to a service first. You can just send any RPC to any service, however the callee will know the PID of who's calling it. I'm not expecting a round-trip RPC to the Permissions Manager per call to check on permissions for EVERY RPC
A capability-based solution will likely be superior to this because it avoids the confused deputy problem. Access to any resource—including a communication channel—should be a privilege, not a right.
I haven't thought about the confused deputy problem.

The Wikipedia article gives an example of asking a server to open a file on your behalf. Could a simple fix be the server asking if a process has access to the file before opening the file? What about in reverse, a service running as User A is being asked by User B's program to access User B's data? What happens if the service is compromised and thinks: "While I have User B's credentials, I'll spy on all of their files?"

You've given me a lot to think about.

Is the confused deputy problem worth trying to solve? Will it lead me down a rabbit whole of complexity?

Maybe we can avoid the problem if we set some simple security rules:
  1. Each process is responsible for itself.
  2. Don't do work for another process (e.g. open a file for them) unless you have a way to check that they actually have permission to do so - and if forget to do this, it's your own fault because (1).
  3. Understand that if you ask another process to do something for you, it might not have the same permissions as you do, because that process is following rule (1).
Last edited by AndrewAPrice on Thu Feb 04, 2021 4:01 pm, edited 1 time in total.
My OS is Perception.
OSwhatever
Member
Member
Posts: 595
Joined: Mon Jul 05, 2010 4:15 pm

Re: Standardized IPC protocol

Post by OSwhatever »

andrew_w wrote:I think it might be better to make your kernel capability oriented rather than having stateless IPC that uses process or thread IDs as destinations. That way, all the kernel has to do is track which capabilities a process has, and managing permissions is left up to whatever process(es) give out capabilities (e.g. a VFS), without any need for the kernel to ask a process on every message or memory mapping. Capabilities also have the advantage of decoupling IPC endpoints from the process/thread structure of the server, which is a good thing because it lets servers have multiple threads to respond to requests and it also makes it easier to restart crashed servers (PIDs/TIDs will change on restart in most systems, but a capability can be held open and given back to the server after it's restarted). It's probably also a good idea to have separate user-mode components for presenting a UI, persistence of permissions on disk, and actually checking the permissions on open.
In capability based systems, what are the primitives in order to distinguish endpoints? As you wrote traditionally it is process ID/thread ID but what is the mechanism in capability based kernels?
Gigasoft
Member
Member
Posts: 856
Joined: Sat Nov 21, 2009 5:11 pm

Re: Standardized IPC protocol

Post by Gigasoft »

Rather than specifying a destination thread and process ID for each message, one would specify a handle that points to an endpoint of a communications channel set up in advance. In a capability system, one would then be able to pass arbitrary handles along with a message, thereby granting the receiver access to the object. There should also be a way to pass handles when starting a new process. For example, they can be passed as parameters in the create process call, or they can be inserted into a process given a process handle.

The parent process is then responsible for connecting the processes it creates.
OSwhatever
Member
Member
Posts: 595
Joined: Mon Jul 05, 2010 4:15 pm

Re: Standardized IPC protocol

Post by OSwhatever »

Gigasoft wrote:Rather than specifying a destination thread and process ID for each message, one would specify a handle that points to an endpoint of a communications channel set up in advance. In a capability system, one would then be able to pass arbitrary handles along with a message, thereby granting the receiver access to the object. There should also be a way to pass handles when starting a new process. For example, they can be passed as parameters in the create process call, or they can be inserted into a process given a process handle.

The parent process is then responsible for connecting the processes it creates.
This sounds much like QNX which uses handles which corresponds to a full duplex IPC channel. However, QNX is not capability based kernel.

Many kernels, especially those which implements full POSIX compatibility tend to take the route where the kernel handles the security between processes. I went the complete other route where each process is responsible for the security and access rights. Basically each process can deny IPC messages depending on the source. This is usually not suitable for POSIX systems and also systems where processes handling their own security is a subject to weakest link i the chain problem.
User avatar
AndrewAPrice
Member
Member
Posts: 2299
Joined: Mon Jun 05, 2006 11:00 pm
Location: USA (and Australia)

Re: Standardized IPC protocol

Post by AndrewAPrice »

I was thinking that you could merge the approaches. Your parent process becomes your Permissions Manager.

Then you could do things such as create a sandboxed ecosystem for your children. Run a program in a sandbox, forward through the real window manager, but only expose to them a mock VFS.
My OS is Perception.
Gigasoft
Member
Member
Posts: 856
Joined: Sat Nov 21, 2009 5:11 pm

Re: Standardized IPC protocol

Post by Gigasoft »

QNX does not qualify, since QNX messages transfer only data, not handles. Neither does Windows: it only allows passing handles to files, synchronization objects and threads. Most importantly, it does not allow passing port handles through ports or in any other manner. Mach, on which OS X is based, on the other hand, falls in this category.

With capability based security, servers do not need to care about the source of a message. The set of rights is implied by the capability itself. This allows the implementation of indirect handles, which restrict the set of rights conferred by another handle. For more complex scenarios, each client endpoint could have a context pointer associated with it, assigned by the server. When the server receives a message, the access mask and context pointer would be received along with it.
OSwhatever
Member
Member
Posts: 595
Joined: Mon Jul 05, 2010 4:15 pm

Re: Standardized IPC protocol

Post by OSwhatever »

Gigasoft wrote:QNX does not qualify, since QNX messages transfer only data, not handles. Neither does Windows: it only allows passing handles to files, synchronization objects and threads. Most importantly, it does not allow passing port handles through ports or in any other manner. Mach, on which OS X is based, on the other hand, falls in this category.

With capability based security, servers do not need to care about the source of a message. The set of rights is implied by the capability itself. This allows the implementation of indirect handles, which restrict the set of rights conferred by another handle. For more complex scenarios, each client endpoint could have a context pointer associated with it, assigned by the server. When the server receives a message, the access mask and context pointer would be received along with it.
In practice handles can just be data as well. What is preventing an operating system to implement all the capabilities in user space and the kernel IPC just remains simple data transfers?
andrew_w
Posts: 19
Joined: Wed May 07, 2008 5:06 am

Re: Standardized IPC protocol

Post by andrew_w »

OSwhatever wrote:In practice handles can just be data as well. What is preventing an operating system to implement all the capabilities in user space and the kernel IPC just remains simple data transfers?
It is possible to implement capabilities on top of stateless IPC, but AFAIK that would require either forcing all IPC to go through an intermediary server (which is inefficient) or requiring individual servers to enforce them (which makes the attack surface way bigger).
Developer of UX/RT, a QNX/Plan 9-like OS
Post Reply