A C++ library that simplifies implementation of process isolation and privilege separation.
The primary goal is to develop a library that provides a C++ API which allows an application creator to define a group of interconnected processes and build the application using these processes. The library takes care of creating the processes, setting up the communication channels and handling the message routing and delivery. This is useful either from a security point of view, as each process can be stripped off priviledges to a minimal required set, or from a reliability point of view, because if a process in a group of separated processes crashes, it won't unexpectedly terminate the other processes.
-
Provide a pre-implemented set of common processes with documented interfaces (e.g. an "ssh-agent" like process, a server process, command execution process, etc.)
-
Experiment with support for monitoring process statistics and analysing process behaviour for anomalies.
The library is in early stages. It's development was triggered by a need to separate various processes inside the daemon component of the USBGuard project. It could have been just implemented inside the project, but the author thinks that it might be generaly useful to other C++ projects that aim for security or reliability.
With current API, it's possible to define a group of processes, start them and use API functions to send and receive unstructured messages to any member of the group. It is also possible to send and receive open file descriptors.
The library has a built-in, thread-safe logger for debugging purposes. To activate it, just set the PGL_DEBUG environment variable to 1 and then, when you run your pgl based application, debugging messages from the library will be written to pgl-debug.<PID>.log files -- one per process. You can use the logger from your app if you wish to do so. Just ensure that the pgl/Logger.hpp header file is included (or pgl/pgl.hpp) and then you can use the PGL_LOG() macro for appending messages to the log:
PGL_LOG() << "Something happened and errno is set to " << errno;A process is implemented as a subclass of the API-provided class, pgl::Process.
In your subclass, you have to implement a method with type signature equal to
that of a standard process entry-point function, int main(int argc, char *argv[]):
class MyProcess : public pgl::Process
{
int main(int argc, char *argv[]);
};From inside that function, you can call API functions to comminucate with other processes of the group:
void messageBusSend(pid_t peer_pid, const std::string& msg);- messageBusSend.
Send a message stored in
msgto a process with PIDpeer_pid. Thepeer_pidcan be set to-1to indicate, that the message should be sent to all running processes in the group.
pid_t messageBusRecv(pid_t peer_pid, std::string& msg);- messageBusRecv.
Receive a message from process with PID
peer_pidand store it inmsg. Thepeer_pidcan be set to-1to indicate that the message can be received from any processes from the group.
pid_t messageBusSendRecv(pid_t peer_pid, const std::string& msg, std::string& msg_reply);- messageBusSendRecv.
Send a message stored in
msgto a process with PIDpeer_pid, wait for a reply and store it inmsg_reply.
void messageBusSendFD(pid_t peer_pid, int fd, const std::string& message = "");- messageBusSendFD.
Send a message with a file descriptor
fdto a process with PIDpeer_pid. Thepeer_pidcan be set to-1to indicate, that the message should be sent to all running processes in the group.
pid_t messageBusRecvFD(pid_t peer_pid, int *fd, std::string *message = nullptr);- messageBusRecvFD.
Receive a message with a file descriptor from a process with PID
peer_pid. Thepeer_pidcan be set to-1to indicate that the message can be received from any process from the group. If a non-NULL pointer to astd::stringobject is provided inmessage, then the message sent along with the fd will be stored there.
If you want to send a message to a specific process, first you have to resolve its name to its current PID. You can do that by using:
pid_t messageBusResolve(const std::string& name);- messageBusResolve.
Resolve a string identifier of a member of the group to the PID of a running
process with that identifier. If there's no active process with such a name,
-1is returned.
int messageBusWait(unsigned int max_wait_usec = 0);- messageBusWait.
Wait for a message. The
max_wait_usecparameter specifies how long, in microseconds, to wait for a message to appear on the bus. If set to 0, the call will block until a message is available or until the wait is interrupted by an external event (e.g. signal). If a message is available, 1 is returned. On interruption, -1 is returned. Ifmax_wait_usecis non-zero and the timeout expired, 0 is returned.
Defining the group is done by instantiating the pgl::Group class inside the
standard main function and registering your pgl::Process sub-classes in the
instance using the addProcess<typename T>(const std::string& name); method of
the pgl::Group class:
int main(int argc, char *argv[])
{
pgl::Group group(argc, argv);
group.addProcess<MyProcess>("MyProcess1");
group.addProcess<MyProcess>("MyProcess2");
return group.run();
}The pgl::Group constructor takes the original argc and argv values. When
a member process is spawned, these values are forwarded to its main() function.
After we are done registering processes, we can start the process group by
calling the run() method of the pgl::Group class. The function returns only
after all member of the group cease to exist on the system. The return value of
the function should be used as the return value of the standard main()
function as it indicates whether the group terminated successfully or not.
Sources of several demo applications that show how to use the API are located in the src/Examples/ sub-directory in the repository. The minprivs demo application shows how to drop all kinds of priviledges and access to OS resources while still be able to use the API. That application assumes that it'll be started under the root user.