thox contexts and interactions

Inter-process communication (IPC) serves, from a process’s point of view, for communicating with the hardware and other processes, allowing them to cooperate to accomplish a task. It is accomplished in thox using two simple mechanisms:

These mechanisms take place in contexts. Contexts are security objects in which these two interactions take place; they allow processes to manage these elements, amongst others:

  • Sandboxing of given processes (by whitelisting or blacklisting RPC endpoints they can call).

  • Logging (by running the process from a process “watcher”).

  • Compatibility with older thox processes (by converting older RPC endpoints into new ones).

They can also correspond to objects, such as file handles or network connections, where the handle is guaranteed to the daemon to be closed since references to given contexts are closed automatically when a process exits.

Regarding contexts, a process:

  • Has a default context, named context 0.

  • Can get access to other contexts when receiving an answer to an RPC call, using os.pull(), or creating one, using os.context().

  • Can share access to a context by answering a call using os.answer(), or creating a new process using os.run().

A context is owned by the process which has created it using os.context(). This process manages the context, that means it manages the routing of the RPC calls and messages, thus the security of it in case it shares this context with multiple processes.

The initial process gets a context managed by the process manager; the initial process can then create contexts for its children, depending on the security it wants to setup. See initd: the thox init daemon for more information.

Todo

Message queues in a different namespace, also in contexts, with the following functions, probably:

  • os.push(ctx, name, info)

  • os.listen(ctx, name)

This requires similar utilities to RPC calls, as we also need routing (although while calls must arrive to one desintation, messages can arrive to multiple).

Todo

What happens on OS shutdown for example, in which order are processes closed if they need to close processes? Can processes have a “kill” event in order to be able to make their last actions (e.g. sending disconnect messages on network connections)?

Events

thox processes are event-driven; the process manager prepares the events for the process to read, with an optional filter depending on what it expects.

Events can be the following:

  • RPC events, such as calls received by the process and answers received from previous calls.

  • Messages from messages queues the process has subscribed to. These messages can represent “real world” events, etc.

Events are represented using os.Event objects, and are pulled using os.pull(). It is possible to pull specific events, such as the answer to a specific call, by specifying additional parameters to os.pull(); by default, it returns the oldest event not gathered by the process.

Remote Procedure Call (RPC)

thos processes communicate mainly in a one-to-one fashion using a remote procedure call protocol.

Picture three processes P1, P2 and P3, where P3 manages the default context of both processes P1 and P2. P1 wants to execute an action using this protocool, and P2 has this function available and wants any other processes using its default context to be able to run it.

In order to represent the action, thox uses RPC names such as my.super.function. When started up, P3 first decides, either for each action or globally, what it wants to do. Some common possibilities are:

  • It provides a fix set of functions, and does not provide any mechanisms to “bind” functions.

  • It transmits all calls from a given context to another, e.g. calls from a context it created to its default context.

  • It allows RPC name binding.

The basic context most processes on thox will encounter is the context provided by initd (see initd: the thox init daemon for more information). This context allows binding through its os.rpc.bind() and os.rpc.unbind() endpoints.

With binding, daemons such as P2 can then route specific RPC calls done on its default context to itself, which means that subsequent calls by any process to my.super.function on the context provided by P3 will result in P2 receiving a call from the said process. Therefore, when making a call to my.super.function to its default context, P1 will receive an answer from P2.

What happens in order during the call is the following:

  • P1 calls the procedure using the rpc call. This actually emits a call to the system using os.call(), which returns a token in the form of a numerical Call IDentifier (CID).

  • P3 gets a call event, bundled with the CID with which to answer, the arguments given by P1, and some additional request information. It finds out that P2 is bound to the given name, and transmits the call to it using os.transmit().

  • P2 gets a call event, bundled with the CID with which to answer, the arguments given by P1, and some additional request information.

  • P2 treats the request accordingly.

  • P2 emits an answer using os.answer(), passing the CID to it, optionally followed by some return values.

  • P1 gets an answer event, with the CID (to distinguish the call to which the answer is for, in case P1 has sent multiple calls).

For binding a name beforehand, P2 uses os.rpc.bind(); it can also unbind a name using os.rpc.unbind().

RPC names

Names are actually a dot-joined collection of Lua Names:

Names (also called identifiers) in Lua can be any string of letters, digits, and underscores, not beginning with a digit and not being a reserved word. Identifiers are used to name variables, table fields, and labels.

Lua 5.3 manual, §3.1

Note that these names have no arbitrary maximum length; indeed, the Lua implementations used in ComputerCraft (Cobalt/LuaJ 2) and OpenComputers (LuaJ 3), does not implement one for names. However, to avoid confusions, RPC names are case-insensitive, which means that fs.getspaceleft and FS.GetSpaceLeft lead to the same endpoint.

Some valid and invalid identifiers are the following:

Valid identifiers

Invalid identifiers

sleep
os.module
how.deep.does.this.go
my.function2
for
123hello
hello.2theworld
my.gawd$

The rationale behind this definition is to be able to integrate these identifiers into native code using the rpc prefix, for example rpc.sleep(5) to emit a synchronous call to the sleep() function. Case insentivity is explained by the confusion that the system-wide difference between fs.GetSpaceLeft and fs.getspaceleft could generate, leading to potential security problems; see typosquatting for a real world problem alike what this mitigation is addressing.

Notice that while this API is asynchronous, most simple programs will only need to call RPC functions synchronously; to simplify this, the user can use the os.rpc object.