Houyhnhnm Computing: Posts tagged 'Security'urn:http-ngnghm-github-io:-tags-Security-html2016-01-03T19:58:27ZChapter 8: Inter-process Communicationurn:http-ngnghm-github-io:-blog-2016-01-03-chapter-8-inter-process-communication2016-01-03T19:58:27Z2016-01-03T19:58:27ZNgnghm
<p>In our discussion about the difference between <a href="/blog/2015/12/25/chapter-7-platforms-not-applications/">Human applications and their Houyhnhnm counterparts</a> (I often pronounce “Houyhnhnm” as “Hunam”), I was intrigued by claims Ngnghm made (I usually call her “Ann”) that communication was much easier between activities of a Houyhnhnm computing system than between applications of a Human computer system. I asked Ann to elaborate on this topic.</p>
<p>It was easy to agree that Human computer systems made communication something much lower level than it could be. But Ann also argued that Humans had very poor algebras and interfaces for users to combine processes. Just what <em>kinds</em> of communication could there even exist besides the ones that already existed on Human computer systems?</p>
<!-- more-->
<h3 id="implicit-communication">Implicit Communication</h3>
<p>From my discussions with Ann emerged the notion of communications being explicit or implicit.</p>
<p>In the case of explicit communication, a process specifically names another process, whether an existing one or a new one to be started; it then opens a communication channel with that other process, and proceeds to exchange data. Explicit communication does exactly what the programmers want (or at least <em>say</em>: even Houyhnhnms have no AI strong enough to <a href="http://www.jargon.net/jargonfile/d/DWIM.html">DWIM</a>); thus programmers control how much complexity they will afford; but it requires tight coupling between the programs (and thus programmers) on all sides of the communication, and is difficult to extend or adapt to suit the dynamic needs of the end-user.</p>
<p>Conversely, communication with other processes can be implicit: something outside some process grabs data from it, and makes it available to some other process. This is the case with copy-pasting, or with piping the standard output of one process into the standard input of another. Implicit communication is controlled by the users of a program rather than by the programmers who write it, and is therefore adapted to <em>their</em> needs. It sometimes require complex support from the programs that partake in it (or, we’ll argue, their meta-programs); but programmers don’t have to worry about programs on the other side, as long as they abide by some general protocol (and keep up with its updates).</p>
<p>Note that implicit vs explicit is a continuum rather than a clear cut distinction: every communication is partly explicit, because it necessarily involves grabbing data that was somehow published by the first process, the publishing of which wasn’t optimized away; and every communication is partly implicit, because it always relies on something in its context to effect that communication, at the meta level (as known from famous paradoxes, no consistent formal system is perfectly self-contained). Another name for this dimension of software design is declarative vs procedural programming: In the declarative approach, programmers describe what is being computed, without specifying how it is going to be computed or how it will be further processed, which will be determined by strategies at the meta level. In the procedural approach, programmers describe the steps of the computation without specifying what is going to be computed, and all the operational semantics remains at the base level.</p>
<p>Houyhnhnms recognize the importance of both aspects of communication, implicit and explicit; meanwhile Humans tend to be blind about the implicit aspect, because they are habitually reluctant to seriously consider anything at the meta level. When Humans tackle implicit communication (and they cannot not do it), they advance reluctantly into a topic about which they are blind; and thus they end up with implicit communication systems simultaneously quite complex and costly for programmers to implement, yet extremely limited in expressive power for the end-user.</p>
<h3 id="the-case-of-copy-paste">The Case of Copy-Paste</h3>
<p>The kind of implicit communication most visible to end-users in Human computer systems is copy-paste: applications interact with a graphical interface, and may allow the user to either copy or cut part of a document being displayed; the clipping is then stored in a global clipboard (with space for a single clip). Another application interacting with the graphical interface may then allow the user to paste the clipping currently in the clipboard into its own document. The two programs may know nothing of each other; as long as they properly partake in the protocol, they will have communicated with each other as per the desires of the end-user. Copy-pasting alone provides user-controllable implicit communication between most applications, and is an essential feature in Human computer systems.</p>
<p>Now, on Human computer systems, copy-paste requires every participating application to specially implement large chunks of graphical interface support. Every application then becomes somewhat bloated, having to include large graphical libraries (which in modern systems can happily be shared between applications); but also having to properly initialize them, follow their protocols, abide by the strictures of their event loop, etc. They have to be able to negotiate with the clipboard server the kinds of entities they can copy and paste, and/or convert between what the server supports and what they can directly handle. This architecture where all features are implemented at the same level of abstraction contributes significantly to the complexity of applications; applications are therefore hard to reason about, brittle and insecure. The overall graphical environment will in turn inherit the unreliability of the applications that partake in it. And despite all this complexity, often some application will fail to support copying for some of the information it displays (e.g. an error message); the feature is then sorely missed as the user needs to copy said information by hand, or falls back to some low-level means of information capture such as screen copy (or memory dump, for more advanced developers).</p>
<p>An interesting exception to the rule of the above paragraph is the case of “console” applications: these applications display simple text to a “terminal emulator” straight out of the 1970s, at which point all the output can be copied for further pasting. The terminal emulator thus serves as the meta-program responsible for presentation of the application output, and handling copy-paste. This comes with many limitations: only plain text is supported, not “rich text”, not images; lines longer than the terminal size may or may not be clipped, or have an end-of-line marker or escape character inserted; selecting more than a screenful may be an issue, though you can sometimes work around it by resizing the terminal or by switching to tiny fonts; standard output and error output may be mixed, and interspersed with output from background programs; layout artifacts may be included (such as spaces to end-of-line, or graphic characters that draw boxes in which text is displayed); etc. Still, the principle of a meta-program to handle display already exists in some Human computer systems; its protocol is just limited, baroque and antiquated.</p>
<p>Houyhnhnm computing systems generalize the idea that presenting data to the end-user is the job of a meta-program separate from the activity that displays the data; this meta-program is part of a common extensible platform, rather than of the self-contained “application” that underlies each activity. The display manager will thus manage a shared clipboard; this clipboard may contain more than just one clip; it may contain an arbitrarily long list of clips (like the Emacs <code>kill-ring</code>). Also, clips can include source domain information, so that the user can’t unintentionally paste sensitive data into untrusted activities, or data of an unexpected kind. The platform manages interactive confirmations, rejection notifications, and content filters, that are activated when users copy or paste data. In these aspects as in all others, the platform can be extended by modules and customized by end-users. Other meta-programs beside the display manager can reuse the same infrastructure: they can use their own criteria to select data from a program’s output; they can use the selected data for arbitrary computations, and store the results into arbitrary variables or data structures, not just a common clipboard; they may consult the history of the selected data, or watch the data continuously as it changes, instead of merely extracting its current value. And the base program doesn’t have to do anything about it, except properly organize its data so that the external meta-programs may reliably search that data.</p>
<h3 id="smoke-on-your-pipe-and-put-that-in">Smoke on Your Pipe and Put That in</h3>
<p>As another instance of implicit communication, one of the great successful inventions of (the Human computer system) Unix was the ability to combine programs through <em>pipes</em>: regular “console” applications possess a mode of operation where they take input from an implicit “standard input” and yield output into an implicit “standard output”, with even a separate “error output” to issue error messages and warnings, and additional “inherited” handles to system-managed entities. A process usually does not know and does not care where the input comes from and where the output is going to: it may be connected to a communication stream with another process, to a terminal, or to a file; the <em>parent process</em> setup the connections before the program started to run.</p>
<p>The parent here plays a bit of the role of a meta level, but this role is very limited and only influences the initial program configuration. (Actually, advanced tools may use <code>ptrace</code>, but it is very unwieldy, non-portable, and inefficient, which may explain why it remains uncommon outside its intended use as a debugging tool.) Still, even within this limitation, Unix pipes revolutionized the way software was written, by allowing independent, isolated programs to be composed, and the resulting compositions to be orchestrated into scripts written in some high-level programming language.</p>
<p>Houyhnhnm computing systems very much acknowledge the power of composing programs; but they are not so restricted as with Unix pipes. They enable composition of programs of arbitrary types, with arbitrary numbers of inputs and outputs all of them properly typed according to some high-level object schema, rather than always low-level sequences of bytes. (Note that low-level sequences of bytes do constitute an acceptable type; they are just rarely used in practice except in a few low-level programs.) These typed inputs and outputs all provide natural communication points that can be used to compose programs together.</p>
<p>Unlike the typical parent processes of Human computer systems, the meta-programs of Houyhnhnm computing systems can control more than the initial configuration of applications. They can at all time control the entire behavior of the base-level program being evaluated. In particular, side-effects as well as inputs and outputs are typed and can be injected or captured. Virtualization is a routine operation available to all users, not just an expensive privileged operation reserved to system administrators.</p>
<h3 id="explicit-communication">Explicit Communication</h3>
<p>There are many obstacles to explicit communication in Human computer systems.</p>
<p>A first obstacle, that we already mentioned in a previous chapter, is the low-level nature of the data that is exchanged with their communication protocols, which constitutes a uniform obstacle to all communications by making them complex, error-prone, and insecure. But these protocols are not low-level only with respect to the data; they are also low-level with respect to communication channels. Human programming languages do not support reflection, and communication channels are selected by passing around <em>handles</em>, low-level first-class objects (typically small integers); this makes it harder to define and enforce invariants as to how channels may or may not be used within a given process: any function having a handle can do anything with it, and handles are often easy to forge; thus you can’t reason about security locally. Houyhnhnm programming languages instead support reflection; thus while they can express the same low-level protocols as above, they tend to (fully) abstract over them and instead expose higher-level protocols, where the channel discipline as well as the data discipline are expressed as part of the types of the functions that exchange data. Communication channel names become regular identifiers of the programming language; the language lets programmers use dynamic binding to control these identifiers; and the usual type-checking and verification techniques apply to enforce protocol invariants not limited to data format.</p>
<p>A second obstacle specific to explicit communication is that to be a legitimate target to such communication, a program must specifically implement a server that listens on a known port, or that registers on a common “data bus”; where this becomes really hard is that to process the connections, the server must either possess some asynchronous event loop, or deal with hard concurrency issues. Unhappily, Human mainstream programming languages have no linguistic support for decentralized event loops, and make concurrency really hard because side-effects in threads can all too easily mess things up. Libraries that implement a centralized event loop are <em>ipso facto</em> incompatible with each other; those that rely on concurrency and a locking discipline are still hard to mix and match, and to avoid deadlocks they require an improbable global consensus on lock order when used by multiple other libraries.</p>
<p>Houyhnhnm programming languages, like the more advanced Human programming languages (including Erlang, Racket, Haskell, OCaml, etc.) both support decentralized event loops (the crucial feature being <a href="http://fare.livejournal.com/142410.html">proper tail calls</a>, and for even more advanced support, first-class delimited continuations), and make it easier by supporting well-typed concurrency abstractions on top of a functional programming core, which is a big improvement. But Houyhnhnm computing systems also make it possible to move these servers completely to a separate meta-program that controls the process you communicate with; thus the base-level process can be written as a simple program, with a very simple semantics, easy to reason about, without any pollution by the server and its complex and possibly incompatible semantics; yet it is possible to tell it to invoke exported functions or otherwise run transactions on its state, by talking to the meta-program that controls it.</p>
<p>A third obstacle specific to explicit communication in Human computer systems is the difficulty of locating and <em>naming</em> one of those target processes available to communicate with. Indeed, inasmuch as communication is explicit, it requires some way to <em>name</em> the party you want to communicate with: a named process (in e.g. Erlang), a numbered port or a named pipe or socket on the current machine (in e.g. Unix), a remote port on a named machine (using TCP/IP), etc. Implicit communication only needs to distinguish between local ports: “standard input”, “standard output”, “file descriptor number 9”, “the graphical display manager” (including its cut-and-paste manager), etc., without having to know what or whom is connected to it on the other side. Reading (or writing to) a file is intermediate between the explicit and implicit: you know the name of the file, but not the identity of who wrote the file (or will read it). Naming a port can also be considered more implicit and less explicit than naming a process.</p>
<p>Now, Human computer systems do not have object persistence, so all their connections and all their names are transient entities that must be reestablished constantly. Human computer systems also have no notion of dynamic environment; there is a static environment, set at the start of a process, but it doesn’t adapt to dynamic changes; or to track dynamic changes, programs can query servers, but then the behavior is either completely unconstrained or highly non-local. You can try to automate this communication, but every program has to handle a vast array of error cases. In any case, local reasoning about dynamic properties is nearly impossible.</p>
<p>Houyhnhnm computing systems have persistence, which means you can give a stable name to a stable activity, and establish a stable connection; they also feature dynamic binding as a language feature that can be used to control the behavior of programs or groups of programs in a structured way. How do they deal with transience, reconnection and unreliability at lower levels of the system? They abstract issues away by introducing a clear distinction between base level and meta level: the base level program is written in an algebra that can assume these problems are solved, with persistent naming and dynamic reconnection both implicitly solved; the meta level program takes care of these issues. Local reasoning on small simple programs (whether at the base or meta level) keeps the overall complexity of the system in check while ensuring robustness.</p>
<h3 id="whats-in-a-name">What’s in a Name?</h3>
<p>At the extreme end, opposite to implicit communication, the communication is so explicit that the system knows exactly what’s on the other side of a communication portal. The inter-process communication can then be reduced to a static function call, and the listening function on the other side can often itself be inlined. And in a Houyhnhnm computing system, this may indeed happen, automatically.</p>
<p>Indeed, when it doesn’t change very frequently, whatever is on the other side of any communication channel can be considered locally constant; then, whichever meta-program handles connecting the communicating parties, whether a linker or JIT, can optimize all communication into function calls, and function calls into more specific instructions; it can then reduce all higher-order functions and indirections to efficient loops, until a change in the connection or in the code invalidates these optimizations.</p>
<p>Of course, sometimes the optimization that makes sense goes the other way, transforming function calls into communication with another process: a process on a CPU might delegate computations to a GPU; an embedded device, including a mobile phone, might rather query a server than compute itself, etc. Thus local CPU cycles can be saved whenever cheaper, faster and/or more energy-efficient resources are available. And there again, a more declarative approach allows meta-programs to automatically pick a better strategy adapted to the dynamic program context.</p>
<p>In the end, factoring the code in terms of base-level and meta-level is an essential tool for division of programming labor: The base-level programmer can focus on expressing pertinent aspects of the program semantics; he can write smaller programs that are simpler, easier to reason about, easier to compose; they can be written in a domain-specific language, or, equivalently, in a recognizable subset of his general-purpose language with well-defined patterns of function calls. The meta-level programmer can focus on implementation strategies and optimizations; he has a relatively simple, well-defined framework to prove their correctness, whether formally or informally; and he can focus on the patterns he is interested in, while leveraging the common platform for all other evaluation patterns, instead of having to reinvent the wheel. Thus, whether the source code for some part of an application is modular or monolithic is wholly independent of whether the implementation will be modular or monolithic at runtime. The former is a matter of division of labor and specialization of tasks between programmers at coding-time; the latter is a matter of division of labor and specialization of tasks between hardware components at runtime.</p>
<p>At every level, each programmer can and must use explicit names each implicitly bound to a value, to abstract any process, function or object that belongs to another programmer. By hypothesis, the programmer never knows for sure what the name will be bound to — though often that other programmer may well be the same programmer in a different role at a different time. Yet the overall system in time can always see all the bindings and statically or dynamically reduce them, efficiently combining all parts of a programs into one. Names allow to express fixed intent in an ontology where the extent will change (the extent being the value of a variable, or the text of a function, etc.); they are superfluous from the perspective of a computer system, because for a computer system any name beside memory addresses and offsets is but a costly indirection that is better done away with; names are important precisely because programming is part of a computing system, where the activities of programmers require abstraction and communication across programmers, across time, across projects, etc.</p>
<h3 id="activity-sandboxing">Activity Sandboxing</h3>
<p>In Houyhnhnm computing systems, proper sandboxing ensures that activities may only share or access data according to the rules they have declared and that the system owner agreed to. In particular, purported <a href="/blog/2015/12/25/chapter-7-platforms-not-applications/">autistic applications</a> are ensured to actually be autistic. Proper sandboxing also means that the system owner isn’t afraid of getting viruses, malware or data leaks via an activity.</p>
<p>Unlike Human computer systems, Houyhnhnm computing systems always run all code in a fully abstract sandbox, as controlled by a user-controlled meta-program. There is no supported way for code to distinguish between “normal” and “virtualized” machines. If the system owner refuses to grant an application access rights to some or all requested resources, the activity has no direct way to determine that the access was denied; instead, whenever it will access the resource, it will be suspended, or get blank data, or fake data from a randomized honeypot, or a notification of network delay, or whatever its meta-level is configured to provide; the system owner ultimately controls all configuration. If the application is well-behaved, many unauthorized accesses may be optimized away; but even if it’s not, it has no reliable way of telling whether it’s running “for real”, i.e. whether it’s connected to some actual resource.</p>
<p>Allowing code to make the difference would be a huge security failure; and any time a monitor in a production system recognizes the attempt by a process to probe its environment or otherwise break the abstraction, a serious security violation is flagged; upon detection, the process and all its associated processes are suspended, up to the next suitably secure meta-level; also the incident is logged, an investigation is triggered, and the responsible software vendor is questioned. — Unless of course, the people responsible for the break in attempt are the system’s owners themselves, or penetration testers they have hired to assess and improve their security, which is a recommended practice among anyone hosting computations controlling any important actual resources.</p>
<p>Note that proper sandboxing at heart has <a href="/blog/2015/11/28/chapter-6-kernel-is-as-kernel-does/">nothing whatsoever</a> to do with having “kernel” support for “containers” or hardware-accelerated “virtual machines”; rather it is all about providing <em>full abstraction</em>, i.e. abstractions that don’t leak. For instance, a user-interface should make it impossible to break the abstraction without intentionally going to the meta-level. You shouldn’t be able to accidentally copy and paste potentially sensitive information from one sandbox to the next; instead, copy and pasting from one sandbox to another should require extra confirmation <em>before</em> any information is transferred; the prompt is managed by a common meta-level below the sandboxes, and provides the user with context about which are the sandboxes and what is the considered content; that the user may thus usefully confirm based on useful information — or he may mark this context or a larger context as authorized for copying and pasting without further confirmations.</p>
<h3 id="protocols-as-meta-level-business">Protocols as Meta-level Business</h3>
<p>Houyhnhnm computing systems resolutely adopt the notion that some tasks are generally the responsibility of a series of (meta)programs that are separate from the ones computing the results; i.e. presenting computation results, combining communicating processes, choosing an implementation strategy for a declarative program, etc. Factoring out the interface at the meta level means that each level can be kept conceptually simple. The system remains <a href="http://fsharpforfunandprofit.com/posts/is-your-language-unreasonable/">“reasonable”</a>, that is susceptible to be reasoned about. It’s enough to assess the security properties only once, abstracting away the specifics of programs using the features.</p>
<p>Thus, in Houyhnhnm computing systems, unlike in Human computer systems, the robustness and timeliness of the system don’t have to depend on every application partaking in the protocol being well-behaved, nor on every programmer working on any such application being steadfast at all times and never making any mistake. There can be no bugs in all the lines of code that the programmers don’t have to write anymore. And updating or extending the protocol is much easier, since it only involves updating or extending the according meta-programs, without having to touch the base-level applications (unless they want to explicitly take advantage of new features).</p>
<p>Moving features from base-level applications to meta-level layers can be justified with all the same arguments why “preemptive multitasking” beats “cooperative multitasking” as an interface offered to programmers: sentient programmers are intrinsically unreliable, any kind of “cooperation” that relies on manually written code to abide by non-trivial invariants in all cases will result in massive system instability. At the same time, “cooperative” beats “uncooperative” as far as implementation is concerned; cooperation is (and actually must be) used under the hood to preserve any non-trivial system invariants — but it can be used automatically and correctly, through a meta-program’s code-generator.</p>
<h3 id="embracing-or-fearing-the-meta">Embracing or Fearing The Meta</h3>
<p>In Human computer systems, there are few features implemented as meta-programs; software libraries are strictly runtime entities, and programmers must manually follow the “design patterns” required to properly implement the protocols supported by those libraries. In Houyhnhnm computing systems, there are plenty of meta-programs; and though they may have a runtime component like libraries, they most importantly include compile-time and/or link-time entities; and they ensure that all runtime code strictly follows all supported protocols by construction. The meta-programs, that display, select, extract, or watch data, use introspection of the program’s state, types and variables; and for reasons of efficiency, they do not re-do it constantly at runtime; yet they do keep enough information available at runtime to recompute whatever is needed when programs change.</p>
<p>Changes in these meta-programs may involve recompiling or otherwise reprocessing every base program that uses them. This meta-processing is deeply incompatible with the traditional Human notion of “binary-only” or “closed source” distribution of software; but that notion doesn’t exist for Houyhnhnms: Houyhnhnms understand that <a href="http://fare.tunes.org/articles/ll99/index.en.html">metaprogramming requires free availability of sources</a>. For similar reasons, Humans who sell proprietary software see a platform based on meta-programming as the Antichrist.</p>
<p>A program that comes without source is crippled in terms of functionality; it is also untrusted, to be run in a specially paranoid (and hence slower) sandbox. Houyhnhnms may tolerate interactive documents that behave that way; they may accept black box services where they only care about the end-result of one-time interactions, at the periphery of their computing systems. But they have little patience for integrating a black-box program into critical parts of their regular platforms; they won’t put it in a position where it has access to critical information, or make it part of any process the failure of which could threaten the integrity of the system. If they care about what a black-box program does, they will spend enough time to reimplement it openly.</p>Chapter 6: Kernel Is As Kernel Doesurn:http-ngnghm-github-io:-blog-2015-11-28-chapter-6-kernel-is-as-kernel-does2015-11-29T04:34:45Z2015-11-29T04:34:45ZNgnghm
<p>I admitted to Ngnghm (whom I call “Ann”) that I was perplexed by Houyhnhnm computing systems (I pronounce “Houyhnhnm” like “Hunam”). To better understand them, I wanted to know what their kernels, libraries and applications looked like. There again, he surprised me by having no notion of what I called kernel or application: the way Houyhnhnm systems are architected leads to widely different concepts; and for the most part there isn’t a direct one-to-one correspondance between our notions and theirs. And so I endeavored to discover what in Houyhnhnm computing systems replaces what in Human computer systems is embodied by the operating system kernel.</p>
<!-- more-->
<h3 id="kernels">Kernels</h3>
<p>“What does an Operating System Kernel look like in a Houyhnhnm computing system?” I asked Ann. She wasn’t sure what I was calling either Operating System or Kernel.</p>
<p>I explained that in a Human computer system, the kernel is a piece of software that handled the hardware resources, and provided some uniform abstractions that isolated users from the hardware details that varied from machine to machine and across time. All the rest of the system is expressed in terms of those abstractions; modern computer hardware ensures through runtime checks that all programs beside the kernel run in a “user mode” that only sees these abstractions; the kernel alone runs in a “kernel mode” that gives it direct access to the hardware. The kernel can also use this hardware support to provide low-level isolation between “processes”: it allows multiple user programs to run at the same time while ensuring that none may interfere with other programs except through said abstractions.</p>
<p>Ann however had trouble distinguishing the kernel from any other program based on my description. The notion of kernel, like most concepts of Human computer systems, was too artifact-oriented and didn’t fit the grid of interaction-oriented Houyhnhnm computing systems. “What is it that a kernel <em>does</em>?” Ann asked me; when she’d know that, she could tell me how their systems implement analogous <em>interactions</em>. And then I was at loss to distinguish exactly what kinds of interaction a kernel does that other pieces of software don’t.</p>
<h3 id="resource-management">Resource Management</h3>
<p>The most obvious thing a kernel does is that it manages and <em>multiplexes</em> resources: it takes some resources, such as CPU time, core memory, disk space, console access, network connections, etc.; and it makes available to multiple programs, either one after the other, or at the same time. It ensures that each program can use all the resources it needs without programs stepping on each other’s toes and corrupting each other’s state.</p>
<p>However, resource management cannot <em>define</em> what a kernel is, since plenty of other components of a computer system also manage resources: All but the simplest programs contain a memory allocator. A database server, or any kind of server, really, manages some kind of records. A sound mixer, a 3D scene renderer, a Window system, or anything worth of being called a system at all, allow multiple entities to coexist, interact with each other, and be perceived, modified, accessed, experienced, by the system’s user.</p>
<p>Houyhnhnms recognize that resource management is an infinitely varied topic; this topic cannot possibly be reduced to a fixed set of resources, but is an inherent aspect of most programs. When they need to explicitly deal with this aspect, Houyhnhnms make it an explicit part of the rich algebra they use to express programs. The simplest idiom for that is to use a proxy, a handle, or some indirect way of naming a resource; programs that use the resource may only go through that handle, while only the program that manages the resource manipulates the underlying details. More advanced idioms include using some variant of what we call <a href="https://en.wikipedia.org/wiki/Linear_logic">linear logic</a>; on some systems, linear logic can also be painfully emulated using monads.</p>
<h3 id="access-control">Access Control</h3>
<p>A kernel also provides some kind of <em>access control</em> to the resources it exposes: for instance, you have to login as a <em>user</em> to access the system; then you can only access those files owned by said user, or explicitly shared by other users.</p>
<p>But there again so does any system-managed access to resources. Moreover, whichever access control a Human Computer System kernel provides is often so primitive that it’s both too slow to be in any code’s critical path and yet too coarse and too inexpressive to match any but the most primitive service’s intended access policies. Therefore, every program must either reimplement its own access control from scratch or become a big security liability whenever it’s exposed to a hostile environment.</p>
<p>Houyhnhnms recognize that access control too is not a fixed issue that can be solved once and for all for all programs using a pre-defined one-size-fits-all policy. It can even less be solved using a policy that’s so simple that it maps directly to a bitmask and some simple hardware operations. Instead, they also prefer to provide explicit primitives in their programming language to let programmers define the access abstractions that fit their purposes; in doing so, they can use common libraries to express all the usual security paradigms and whichever variant or combination the users will actually need; and these primitives fit into the algebra they use to manage resources above.</p>
<h3 id="abstraction">Abstraction</h3>
<p>A Human Computer System kernel (usually) provides <em>abstraction</em>: all programs in the system, beside the kernel itself, are <em>user</em> programs; their computations are restricted to <em>only</em> be combinations of the primitives provided by the <em>system</em>, as implemented at runtime by the kernel. It is not possible for user programs to subvert the system and directly access the resources on top of which the system itself is built (or if it is, it’s a major security issue to be addressed as the highest emergency). The system thus offers an abstraction of the underlying hardware; and this abstraction offers portability of programs to various hardware platforms, as well as security when these programs interact with each other. More generally, abstraction brings the ability to reason about programs independently from the specificities of the hardware on which they will run (“abstracting away” those specificities). And this in turn enables <em>modularity</em> in software and hardware development: the division of labor that makes it possible to master the otherwise unsurmountable complexity of a complete computer system.</p>
<p>Now and again, abstraction is also what any library or service interface provides, and what every programming language enforces: by using the otherwise opaque API of the library or service, programmers do not have to worry about how things are implemented underneath, as long as they follow the documented protocol. And by using a programming language that supports it, they can rely on the compiler-generated code always following the documented protocol, and they don’t even have to worry about following it manually: as long as the program compiles and runs, it can’t go wrong (with respect to the compiler-enforced protocols). Abstraction in general is thus not a defining activity of an operating system kernel either; and neither is abstraction of any of the specific resources it manages, that are often better abstracted by further libraries or languages.</p>
<p>Houyhnhnms not only reckon that abstraction is an essential mechanism for expressing programs; Houyhnhnms also acknowledge that abstraction is not reserved to a one single “system” abstraction to be shared by all programmers in all circumstances. Rather, abstraction is an essential tool for the division of mental labor, and is available to all programmers who want to define the limits between their respective responsibilities. The program algebras used by Houyhnhnms thus have a notion of first-class programming system (which includes programming language as well as programming runtime), that programmers can freely define as well as use, in every case providing abstraction. Since they are first-class, they can also be parametrized and made from smaller blocks.</p>
<p>Note, however, that when parametrizing programming systems, it is important to be able to express <em>full</em> abstraction, whereby programs are prevented from examining the data being abstracted over. A fully abstracted value may only be used according to the interface specified by the abstract variable type; thus, unless that abstract type explicitly includes some interface to inspect the actual value’s type or to deconstruct its value according to specific match patterns, the client code won’t be able to do any of these, even if in the end the actual value provided happens to be of a known type for which such operations are available. A dynamic language may implement it through opaque runtime proxies; a static language may provide this feature through static typing; some languages, just like <a href="http://www.csc.villanova.edu/~japaridz/CL/">computability logic</a>, may distinguish between “blind” quantifiers and regular “parallel” or “choice” quantifiers. In any case, the fragment of code in which a full abstraction holds is prevented from peering inside the abstraction, even if the language otherwise provides reflection mechanisms that can see through regular abstractions. Of course, when turtling down the tower of implementations, what is a completely opaque full abstraction at a higher level may be a fully transparent partial abstraction at a lower level; that’s perfectly fine — the lower-level, which cannot be accessed or modified without proper permissions, is indeed responsible for properly implementing the invariants of the higher-level.</p>
<h3 id="enforced-and-unenforced-abstractions">Enforced and Unenforced Abstractions</h3>
<p>There is one thing, though, that kernels do in Human computer systems that other pieces software mostly don’t do — because they mostly can’t do it, lacking system support: and that’s <em>enforcing</em> full abstraction. Indeed, in a Human computer system, typically, only the operating system’s invariants are enforced. They are enforced by the kernel, and no other piece of software is allowed to enforce anything. If a process runs as a given user, say <code>joe</code>, then any other process running as <code>joe</code> can do pretty much what it wants to it, mucking around with its files, maybe even its memory, by attaching with a debugger interface, etc. If a user is allowed to debug things he runs at all (and he probably should be allowed), then all processes running as that user are allowed, too. Users in Unix or Windows can’t create sub-users that they control, in which they could enforce their user-defined invariants. Any failed invariant potentially puts the entire system at risk, and any security breach means everything the user does is affected (which on single-user computers, means everything worthwhile on the computer). That subsystems shall not break their own or each other’s invariants thus remains a pure matter of convention: the kernel will not enforce these invariants at all; they are enforced solely by myriads of informal naming conventions, manual management by system administrators, and social pressure for software developers to play well with software developed by others. Any bug in any application exposed to the internet puts the entire system at risk.</p>
<p>There does exist a tool whereby user-defined invariants can be enforced, of sorts: machine emulation, machine virtualization, hypervisors, containers, user-level filesystems, etc., allow to run an entire human machine with its own kernel. However, except for the most costly and least powerful strategy, emulation, that is always available, these tools are not available for casual users or normal programs; they are heavy-weight tools that require system administrator privileges, and a lot of setup indeed. Still, they exist; and with these tools, companies with a large expensive engineering crew can enforce their company-wide invariants; they can thus enjoy the relative simplicity that comes when you can reason about the entire system, knowing that parasitic behaviors have been eliminated, because they are just not expressible in the <a href="https://en.wikipedia.org/wiki/Unikernel">unikernels</a> that are run inside the managed subsystems.</p>
<p>Houyhnhnms recognize that the invariants that ultimately matter in a computing system are never those that underlie any “base” system; instead, they are always those of the overall system, the “final” applications, as experienced by users. To them, the idea that there should be a one privileged “base” system, with a kernel that possesses a monopoly on invariant enforcement, is absurd on its face; the invariants of a system low-level enough to implement all the programs that users may care about are necessarily way too low-level to matter to any user. In Houyhnhnm computing systems, virtualization is a basic ubiquitous service that is universally relied upon; each activity is properly isolated and its invariants cannot be violated by any other activity, except those that explicitly run at its meta-level.</p>
<p>What’s more, Houyhnhnms understand that when building software components and applications, programmers almost never want to start from that “base” system of a low-level machine, however virtualized, but want to start from as high-level a system as they can afford. Therefore, Houyhnhnms have first-class notions for computing systems and for implementing a computing system based on another computing system (again, in terms closer to notions of a human computer systems, a computing system includes both a programming language and a runtime virtual machine). Which functionality is virtualized, and which is just inherited unmodified from the lower-level system, can thus be decided at the highest level that makes sense, keeping the overall system simpler, easier to reason about, and easier to implement efficiently — which is all one and the same.</p>
<p>Certainly, some technical enforcement cannot wholly replace social enforcement: some invariants are too expensive to enforce through technical means, or would require artificial intelligence to do so, which Houyhnhnms don’t possess more than Humans. But at least, Houyhnhnms can minimize the “surface of attack” for technical defects that can possibly violate desired invariants, by making such attacks impossible without a meta-level intervention; and those meta-level interventions that are necessary can be logged and reviewed, making for more effective social enforcement as well as for general reproducibility and debuggability.</p>
<h3 id="runtime-and-compile-time-enforcement">Runtime and Compile-time Enforcement</h3>
<p>In a Human computer system, The Kernel possesses a monopoly on invariant enforcement, and only enforces a static set of invariants; it does so by being an expensive middleman at runtime between any two components that communicate with each other or use any hardware device. In terms of invariant enforcement, this is simultaneously extremely unexpressive and quite expensive.</p>
<p>Houyhnhnm computing systems, on the other hand, have a decentralized model of invariant enforcement. Every user specifies his invariants by picking a high-level language as the base for his semantics, and using this language to define further computing elements and their invariants. Most invariants can be enforced statically by the high-level language’s compiler, and necessitate no runtime enforcement whatsoever, eschewing the cost of a kernel. When multiple components need to communicate with each other, the linker can similarly check and enforce most invariants, and eschew any runtime enforcement cost.</p>
<p>Well-behaved programming language implementations can therefore manipulate low-level buffers directly without any copying, when producing video or sound; the result is real-time performance without expensive runtime tricks — or rather, performance precisely by the absence of expensive runtime tricks. When the user requests causes a change in the circuit diagram, the code may have to be unlinked and relinked: thus, relinking will happen when users add or remove a filter between the sound producers and the actual audio output, or similarly introduce some window between graphic drawers and the actual video output. But this relinking can happen without any interruption in the music, with an atomic code switch at a time the buffers are in a stable state.</p>
<p>Certainly, any available hardware support to optimize or secure virtualization can and will be used, wherever it makes sense. But it isn’t the exclusive domain of a One Kernel enforcing one static set of invariants. Rather, it is part of the panoply of code generation strategies available to compilers targetting the given hardware platform. These techniques will be used by compilers when they are advantageous; they will also be used to segregate computing systems that do not mutually trust each other. But what matters most, they are not foundational system abstractions; the computing interactions desired by the users are the foundational system abstractions, and all the rest is implementation details.</p>
<h3 id="bootstrapping">Bootstrapping</h3>
<p>The last thing (or, depending on how you look at it, first thing) that a Kernel does in a Human Computer System is to <em>bootstrap</em> the computer: The Kernel will initialize the computer, detect the hardware resources available, activate the correct drivers, and somehow publish the abstracted resources. The Kernel will take the system from whatever state the hardware has it when it powers up to some state usable by the user programs at a suitable level of abstraction.</p>
<p>As always, between the firmware, the boot loader, The Kernel, the initialization service manager, the applications that matter, plus various layers of virtualization, etc., the task of initializing the system is already much less centralized even in Human computer systems than the Human “ideal” would have it. Houyhnhnms just do away with this not-so-ideal ideal. They consider that what matters is the state in which the system is ready to engage in whichever actual interactions the user is interested in; anything else is either an intermediate step, or is noise and a waste of resources — either way, nothing worth “blessing” as “the” “base” system. Instead, automatic snapshotting means that the time to restart a Houyhnhnm system is never more than the time to page in the state of the working memory from disk; only the first run after an installation or update can take more time than that.</p>
<p>As for the initial hardware resources, just like any resources visible in a system, they are modeled using linear logic, ensuring they have at all times a well-defined owner; and the owner is usually some virtual device broker and multiplexer that will dynamically and safely link, unlink and relink the device to its current users. Conversely, the users will be linked to a new device if there is a change, e.g. because hardware was plugged in or out, or because the system image was frozen on one hardware platform and thawed on a different one. With the ability to unlink and relink, Houyhnhnm computing systems can thus restart or reconfigure any subsystem while the rest of the system is running, all the while <a href="/blog/2015/08/03/chapter-2-save-our-souls/">persisting any state worth persisting</a>. This is quite unlike Human computer systems, that require you to reboot the entire system any time a component is stuck, at which point you lose the state of all running programs.</p>
<h3 id="polycentric-order">Polycentric Order</h3>
<p>In the end, it appeared that once again, the difference of approach between Humans and Houyhnhnms led to very different architectures, organized around mutually incommensurable notions. Humans think in terms of fixed artifacts; Houyhnhnms think in terms of evolving computing processes. My questions about some hypothetical fixed piece of software in their computing architecture were answered with questions about some hypothetical well-defined patterns of interactions in our computer architecture.</p>
<p>Houyhnhnm computing systems do not possess a one single Kernel; instead they possess as many “kernels” as there are computing subsystems and subsubsystems, each written in as high-level a language as makes sense for its purpose; and the set of those “kernels” continually changes as new processes are started, modified or stopped. Resource management is decentralized using linear logic and meta-level brokers, linking, unlinking and relinking. Invariant enforcement, though it may involve runtime checks, including hardware-assisted ones, is driven primarily by compile-time and link-time processes. Overriding invariants, while possible, requires special privileges and will be logged; unlike with Human computer systems, processes can’t casually interfere with each other “just” because they run with the same coarse “user” privilege.</p>
<p>Humans try to identify an artifact to buy or sell; Houyhnhnms look for processes to partake in. Humans have static understanding of relations between artifacts; Houyhnhnms have a dynamic understanding of interactions between processes. Humans use metaphors of centralized control; Houyhnhnms use metaphors of decentralized ownership. Humans think of enforcement as done by a superior third-party; Houyhnhnms think of enforcement as achieved through mutually agreeable contracts between equally free parties. Humans see all resources as ultimately owned by the Central entity and delegated to users; Houyhnhnms see resources as being used, shared or exchanged by independent processes. I could see a lot of ways that the paradigm of Human computer systems fit in a wider trend of patterns in which to conceive of social and political interactions. Yet, I resisted the temptation of asking Ann about the social and political context in which Houyhnhnm computing systems were being designed; at least for now, I was too deeply interested in figuring out the ins and outs of Houyhnhnm computing to be bothered by a digression into these far ranging matters. However, I did take stock that there was a lot of context that led towards the architecture of Human computer systems; and I saw that this context and its metaphors didn’t apply to Houyhnhnm computing, and that I needed to escape from them if I wanted to better understand it.</p>