Lessons of Manipulating Authority at the Exits(*** To be written) Veracity vs. Representational FreedomAs you may have noticed, serializing an object produces a string that often looks like the result of printing the object. Indeed, the two strings are often identical. E objects print by a recursive traversal using .__printOn(out) messages. For present purposes these may as well be Java's .toString() messages. Given mutual suspicion, then, like serialization, such a printing framework also composes knowledge by the rule-based mixing of intentions expressed by mutually suspicious interests. Why is our serialization algorithm so much more complicated than these? Is it the representational freedom provided by our serialization framework? Hardly. Our serializer spends very little code on providing customization hooks. The representational freedom it provides is mostly an unavoidable consequence of having to ask each object to portray its state, and having to treat each answer as the truth. Besides, the printing frameworks provide much greater representational freedom -- they directly ask each object to report its depiction, leaving it to each object to handle the traversal starting from itself. Since a depiction is only bits, this enables each object to report whatever depiction of itself it wishes, subject only to the limits of what that object knows. (By "knows", we include what it can feasibly compute.) But this is precisely the problem. Such full representational freedom would make serialization useless for the mission we set out on: We seek to serialize in the capability system on one side of a barrier, and unserialize in the capability system on the other side, in order to stitch these together into a virtual capability system that transcends these barriers. To succeed, our connective fabric must not only operate within capability constraints, it must also enforce these constraints within the illusion it creates. If it provides the objects with the wrong kinds of representational freedom, it cannot enforce these constraints. Our serialization framework strikes an interesting balance between the full veracity guaranteed by literal realism and the full representational freedom provided by conventional object printing frameworks. It provides approximately all the representational freedom we know how to allow while still providing the additional veracity guarantee we require: that an object cannot portray itself as holding more authority than it actually has. (By "has", we include that authority it can feasibly obtain.) Otherwise, a matching unserializer may inadvertently grant the object's reconstruction more authority than it should have. An Expository File SystemIn this chapter, we use the principles behind E's file system abstractions as examples. However, for legacy support reasons, E's actual file system is implemented in Java. We present here a simplified variant -- the "pile" system. The authority exercised by the pile system is implemented trivially by wrapping the actual file system abstractions; but the pile system handles all its own serialization-relevant logic. Our first version doesn't handle serialization issues at all: ? pragma.syntax("0.8")# E sample def makePileUriGetter(<file>) :near { def <pile> { /** To get a directory, have absPath end in '/' */ to get(absPath :String) :any { if (absPath.endsWith("/")) { def directory { to get(relPath :String) :any { # shorthand for pile__uriGetter.get(absPath + relPath) <pile>[absPath + relPath] } } } else { # shorthand for file__uriGetter.get(absPath) def file := <file>[absPath] # Note that the underlying file__uriGetter already disallows paths # containing ".." segments, so we need not worry about upward # navigation. require(!file.isDirectory()) def normalPile { /** Return the file's contents as a String */ to getText() :String { file.getText() } /** Set the file's contents to newText */ to setText(newText :String) :void { file.setText(newText) } } } } } } The following step can only be done with access to a file__uriGetter. Updoc scripts, like .e scripts, evaluate in the privilegedScope, which contains binding to powerful capabilities normally conveying all the authority given to this process by the underlying OS. In particular, the file__uriGetter is normally bound to an object conveying all the current user's authority to the file system. ? def <pile> := makePileUriGetter(<file>) # value: <pile__uriGetter> Like the file__uriGetter, the pile__uriGetter conveys all the read/write authority held by the current user to the file system. ? var ehomePath := interp.getProps()["e.home"] ? if (! ehomePath.endsWith("/")) { > ehomePath += "/" > } ? ehomePath # value: "c:/Program Files/erights.org/" ? def <ehome> := <pile>[ehomePath] # value: <directory> ? <ehome:eprops.txt>.getText().size() # example value: 13898 The expression interp.getProps()["e.home"] returns the absolute path of the directory where you installed E. The script above reads the value of the eprops.txt file in this directory and tells you its size as a string (number of characters, not bytes). It could have as easily overwritten this security-critical file of yours. Be careful when running other people's updoc scripts! (If you updoc this page, don't be surprised when it fails because of a different value. 13898 is simply the size this file happens to be on my system. Updoc needs to be made more tolerant.) Unforgeable Exit NamesUpdoc starts afresh with each chapter, so we must redo our example setup: ? def makeSurgeon := <elib:serial.makeSurgeon> ? def surgeon := makeSurgeon.withSrcKit("de: ").diverge() We now make pile__uriGetter into a named exit point, by adding it to both scope and unscope: ? surgeon.addExit(<pile>, "pile__uriGetter") We can now show by example why printing is insufficiently constrained: # Must quote it because it may contain non-URI characters, like space. ? def quotedPath := ehomePath.quote() # value: "\"c:/Program Files/erights.org/\"" ? def iAmEHome1 { > to __printOn(out) :void { > out.print(`<pile>[$quotedPath]`) > } > } # value: <pile>["c:/Program Files/erights.org/"] Here iAmEHome1 prints itself as having authority derived from the pile__uriGetter, even though iAmEHome1 (we assume) does not have such authority. ? def allegedDepiction := "de: " + E.toString(iAmEHome1) # value: "de: <pile>[\"c:/Program Files/erights.org/\"]" ? def <stolenHome> := surgeon.unserialize(allegedDepiction) # value: <directory> ? <stolenHome:eprops.txt>.getText().size() # example value: 13898 As we see above, unserializing iAmEHome1's printed form in a scope where pile__uriGetter is bound as above creates an object equivalent to ehome__uriGetter. But how is this different from iAmFive portraying itself to be 5, which is also a lie, and which our serialization framework intentionally allows? The difference is that iAmFive has access to 5 (or shows that it may feasibly compute 5), so although it is misrepresenting what it is, it cannot misrepresent itself to possess more authority than it actually has. Let's create an iAmEHome2 that use this technique to impersonate ehome__uriGetter during serialization. ? def iAmEHome2 { > to __optUncall() :__Portrayal { > [<pile>, "get", [ehomePath]] > } > } # value: <iAmEHome2> ? iAmEHome2.__optUncall() # value: [<pile__uriGetter>, "get", ["c:/Program Files/erights.org/"]] ? def iAmEHome2Depiction := surgeon.serialize(iAmEHome2) # value: "de: <pile>[\"c:/Program Files/erights.org/\"]" ? def dir := surgeon.unserialize(iAmEHome2Depiction) # value: <directory> ? dir.get("eprops.txt").getText().size() # example value: 13898 Like iAmEHome1, iAmEHome2 misrepresents itself as being equivalent to ehome__uriGetter. Within the printing framework, iAmEHome1 was able to misrepresent itself despite its lack of the authority it pretends to have. (Which is why one should generally not evaluate or unserialize an object's printed form.) Using the serialization framework, iAmEHome2 can misrepresent itself only as an object it demonstrates it could have made. Without access to knowledge and authority adequate to make the equivalent of the ehome__uriGetter, iAmEHome2 cannot successfully misrepresent itself as that object -- it cannot fool the surgeon into unserializing such an object in lieu of itself. Why not? How can we have confidence in this impossibility? Syntactic shorthands aside, there are only three sources of names in a Data-E expression: free variable names (imports), temporary variable names (temps), and message names (verbs).
Other systems [ref Rees] have also used an inverse scope table to delimit the subgraph on the right -- to cut off traversal at enumerated instances, leaving named exits behind; and where these named exits were then reconnected on unserialization using a matched scope table. However, these previous systems have generally provided additional representational freedoms that unknowingly enabled forged authority claims. The invention of the Gordian Surgeon is to solicit from each object only a portrayal of itself in terms of other objects -- never names -- and to depict named exits based only on unscope lookup -- an unforgeable demonstration of held authority. Why and when is this property important? Comity Among NationsComity [...] the courtesy by which nations
recognize within their own territory By the practice known as Comity Among Nations a traveler's new home may extend to him those local rights most closely corresponding to the rights he had in his old one. When practiced routinely, each nation's separate systems of rights and laws are roughly and imperfectly stitched together into a virtual worldwide legal system, giving travelers a certain illusion of uniformity, and their rights a certain measure of location independence. The willingness and ability to extend comity, and the nature of comity extended, will differ on a case by case basis, constrained by each nation's internal policies and by the nature of its relationship with each potential counterparty. The resulting rough virtual legal system emerges in a fully decentralized manner from all these local decisions, without need for any central body. Once upon a time, a U.S. doctor, faker, lawyer, and CIA agent with a stereo system move to France. Honoring the principle of comity among nations, the French grant the doctor a corresponding license to practice medicine -- the fact that she acquired that right in the U.S. is taken as adequate evidence that she should hold the most closely corresponding right in her new home. Like the doctor, the faker presents a U.S. license to practice medicine, but a forged one. If this mere piece of paper is taken at face value, then the faker is also granted a license to practice in France. The lawyer is not so lucky -- whereas U.S. and French biology are very similar, their legal systems are very different, so a lawyer able to function in one environment may not be assumed competent to function in the other. The veracity and competence of the CIA agent is not in doubt, but he is also not granted a corresponding position in the French intelligence agency, for either of two reasons enumerated below. Below we enumerate various consideration that effect whether and how comity is granted. While many of these considerations may involve decision processes of potentially great complexity, well beyond that of the present work, the point here is that this whole range of policy choices can be cleanly expressed using the simple mechanism of choosing bindings for the scope and unscope tables. Willing and Able to Extend ComityThis is the happy case -- the French agency is both willing and able to grant comity to the doctor, and are properly satisfied that she did have such authority, so they grant her corresponding authority. False PretensesThe French also grant a license to the faker, but this case isn't so happy, at least for the faker's patients -- those whose interests the comity-granting agency is charged with protecting. Like the doctor and the faker, our iAmEHome2 and iAmEHome1 unserialized in the same way, acquiring the same authority. What differed is how they were serialized -- where their diplomas came from -- which, unlike the French government, our unserializer is not in a position to determine. Rather, the programmer must jointly design which authorities an unserializer may grant, what reasons he has for confidence that the depiction comes from a matched serializer, and what sort of trust he is willing to invest in that serializer such that he's willing to have authorities granted to reconstructed objects on its say so. The two extreme cases should make this clearer:
Limited Willingness to Extend ComityFor very different reasons, the French are unwilling to extend comity to the lawyer and the secret agent. However, they represent this policy choice in the same way: they do not populate the scope with the bindings that would grant these authorities, even if they have these authorities and they know the incoming objects may fail to work unless these authorities are granted. Better fail-stop than sorry. Serializers and unserializers don't have to be perfectly matched if unserialization is allowed to fail. However, they still must be confident they agree on the meaning of the names they have in common. Limited Authority to Extend ComityThe French agency charged with making comity choices may not have jurisdiction over the French intelligence agency. Even if it were willing to extend comity to the arriving agent, it may not be able to. One cannot populate the unserializer's scope with bindings one does not have. Virtual Comity in order to Diminish AuthorityOn moving into his apartment, our agent sets his stereo back up. He doesn't know or care how it works inside, so, before unplugging all the wires, being a meticulous agent type of guy, he labeled what each was hooked to. OK, this one goes to the left speaker ..., the internal subgraph is now hooked back up. What of the power cord? They've got a funny socket here that provides too much power. Even though it is the connection most closely corresponding to what it used to be connected to, it provides more power than he's willing to grant to the stereo system. So he pulls out his power adapter. A power adapter intermediates between the amount and kind of power appropriate for the stereo system and which it is built to expect vs. the amount and kind of power made available by connectors in the stereo's new environment. The adapter provides only diminished power to the stereo. The closest adequate matching authority in the new environment may be too far in excess of the originally held authority. Comity should usually not increase the authority held by an incoming object. (Using conventional notions of typing and compatibility, these can be difficult to detect across versions, since an otherwise equivalent object that conveys more authority than its predecessor would classically be considered to be an upwards compatible subtype of its predecessor. We need authority-cognizant theories of compatibility and subtyping) When setting up an unserializer, to express the policy choice of granting approximately par authority but not increased authority, one will often need to create and populate the scope with adapters -- facets on the newly available authorities that diminish their powers to approximate the corresponding original authority. Virtual Comity in order to Simulate OperationVirtual comity is not just a matter of adapting authority, but also of functionality. The lawyer carries around a Berlitz phrasebook, the doctor an English-metric conversion table. The agent a babel fish. Each is able to deal with their new world in a somewhat degraded fashion, but using an interface they already know how to operate. In the extreme of this technique, we find ourselves simulating (rehosting) a previous environment with all its ideosynchracies. Without preparation, this may be the most tractable way to revive old state and have it continue to work. (In the chapter Persistence and Upgrade, we show how preparation can mostly avoid this worst case.) Notice that our directory objects above have the same protocol as the pile__uriGetter itself. As we will see once we have a more complete pile system, if directory <pile:/foo/bar/> is installed in an unserializer's scope as the "pile__uriGetter", then a serialized claim to the authority "<pile:/zip/zap.txt>" would unserialize to authority to the actual <pile:/foo/bar/zip/zap.txt>. This is the familiar Unix chroot trick in a new guise. It is virtual comity both to diminish authority and to simulate operation. Subgraph Transplant SurgeryAlthough our iAmEHome2 above serializes and unserializes as the equivalent of our ehome__uriGetter, our ehome__uriGetter itself doesn't. In fact, it doesn't serialize at all: ? surgeon.serialize(<ehome>) # problem: Can't uneval <directory> Could we repair this by having each directory use the same technique used by iAmEHome2? This would involve simply adding to each directory the method to __optUncall() :__Portrayal { [<pile>, "get", [absPath]] } However, this would destroy a crucial property of the directory abstraction -- it would no longer be diminishing the authority of the pile__uriGetter by only granting its clients access to a subtree. Instead, any of its clients could simply call __optUncall() on a directory, and obtain access to that directory's entire pile system. Such a method would make a directory unconditionally transparent. To repair this correctly requires the selective transparency technique we're postponing to the next chapter, so instead, here, we cheat. In the following, assume that any message names beginning with "pkg_" are somehow made to behave like Java's package scope -- assume these messages can only be sent by other code within the same package-or-something, but not by separately trusted clients. Let's proceed pretending we have selective transparency. # E sample def makePileUriGetter(<file>) :near { def <pile> { to optUncall(obj) :nullOk[__Portrayal] { if (Ref.isNear(obj) && obj.__respondsTo("pkg_getOptUncall", 0)) { obj.pkg_getOptUncall() } else { null } } to optUnget(obj) :nullOk[String] { # The pattern ==<expression> is true iff it is matched against a # specimen that's == to the value of this expression. # The pattern `get` matches only the string "get" if (<pile>.optUncall(obj) =~ [==<pile>, `get`, [path]]) { path } else { null } } /** To get a directory, have absPath end in '/' */ to get(absPath :String) :any { if (absPath.endsWith("/")) { def directory { to pkg_getOptUncall() :__Portrayal { [<pile>, "get", [absPath]] } to optUncall(obj) :nullOk[__Portrayal] { # The pattern `$absPath@relPath` matches a string iff the string # begins with the value of absPath, in which case it defines relPath # to hold the rest of the string. if (<pile>.optUncall(obj) =~ [==<pile>, `get`, [`$absPath@relPath`]]) { if (relPath.size() >= 1) { # If it is under this directory, then portray it relative to # this directory. Under no circumstances allow the # pile__uriGetter to be exposed. return [directory, "get", [relPath]] } } # Let some other uncaller handle it, if any. null } to optUnget(obj) :nullOk[String] { if (directory.optUncall(obj) =~ [==directory, `get`, [path]]) { path } else { null } } to get(relPath :String) :any { # shorthand for pile__uriGetter.get(absPath + relPath) <pile>[absPath + relPath] } } } else { # shorthand for file__uriGetter.get(absPath) def file := <file>[absPath] # Note that the underlying file__uriGetter already disallows paths # containing ".." segments, so we need not worry about upward # navigation. require(!file.isDirectory()) def normalPile { to pkg_getOptUncall() :__Portrayal { [<pile>, "get", [absPath]] } /** Return the file's contents as a String */ to getText() :String { file.getText() } /** Set the file's contents to newText */ to setText(newText :String) :void { file.setText(newText) } } } } } }
? def <pile> := makePileUriGetter(<file>) ? def <ehome> := <pile>[ehomePath] ? surgeon.addExit(<pile>, "pile__uriGetter") But a directory or normalPile cannot yet be serialized, since they don't portray themselves. We need to add an uncaller, like the pile__uriGetter, that knows how to portray them. addUncaller(..) inserts its argument at the beginning of this surgeon's uncallers list. ? surgeon.addUncaller(<pile>) ? surgeon.serialize(<ehome>) # value: "de: <pile>[\"c:/Program Files/erights.org/\"]" ? def eprops := <ehome:eprops.txt> # value: <normalPile> ? surgeon.serialize(eprops) # value: "de: <pile>[\"c:/Program Files/erights.org/eprops.txt\"]" We have also made each directory act as an uncaller that knows how to uncall directories and normalPiles within its subtree. In order for a directory to have any affect, we must add it ahead of the pile__uriGetter, since the pile__uriGetter will portray anything any of its directories will portray. ? surgeon.addUncaller(<ehome>) ? surgeon.serialize(<ehome>) # value: "de: <pile>[\"c:/Program Files/erights.org/\"]" ? surgeon.serialize(eprops) # value: "de: <pile>[\"c:/Program \ # Files/erights.org/\"][\"eprops.txt\"]" Cute, but it doesn't do much good until the directory is also added to the scope and unscope tables. ? surgeon.addExit(<ehome>, "ehome__uriGetter") ? surgeon.serialize(<ehome>) # value: "de: <ehome>" ? def depiction := surgeon.serialize(eprops) # value: "de: <ehome:eprops.txt>" ? def eprops2 := surgeon.unserialize(depiction) # value: <normalPile> ? eprops2.getText().size() # example value: 13898 Like an actual surgeon extracting an organ for transplant, we see we have some freedom in where to cut. We should chose those cut points that increase the likelihood that the extracted organ, when reattached to corresponding cut points in a new body, will result in an overall working system. These choices must often be made without knowledge of the specifics of the recipient body, but just from a general knowledge of the ways in which one body tends to differ from another. Since the files in the E installation directory have a special significance to E, and since we know this directory may be placed at different absolute paths during installation, cutting at this directory as an additional exit point is plausible, to enable, for example, a checkpoint taken at one time to be more likely to revive after the user uninstalls E and reinstalls it at a different location. (To see these choices as cut points in the graph, imagine our pile system implemented as if each directory or normalPile were local name + a pointer to the immediate parent directory, or pile__uriGetter for the root.) Leaving our organ metaphor, this freedom is also crucial for flexible handling of authority issues. The subsystem arranging a matched serializer / unserializer pair may not have access to the file system as a whole, but only access to certain subdirectories. Clearly, the unserializer cannot unserialize something to have access to a directory it itself does not have. A serializer using this pattern will also fail to serialize access to files outside the directories it names as exit points. With this pattern, an original object that held access to the file named "c:/Program Files/erights.org/eprops.txt" only reconstructs with access to whatever file this name is redirected to by the above technique. It cannot retain access to whatever file happens to have the original name. Together, these techniques effectively extend the unscope and scope tables into a hierarchical name space, in which a virtual exit name is a path rooted in an actual exit name. The Loader PatternIgnoring our cheat till the next chapter, our pile system has been following the Loader Pattern. A Loader is defined by the following interface declaration interface Loader { to get(name :String) :any /** If child can be gotten from this Loader with a get, return the needed argument string. */ to optUnget(child) :nullOk[String] /** Actually, optUncall is defined in Uncaller, and Loader is a subtype of Uncaller. */ to optUncall(specimen) :nullOk[__Portrayal] } The Loader Pattern has the following elements.
? <pile>.optUncall(eprops) # value: [<pile__uriGetter>, "get", \ # ["c:/Program Files/erights.org/eprops.txt"]] ? <pile>.optUnget(eprops) # value: "c:/Program Files/erights.org/eprops.txt" ? <ehome>.optUncall(eprops) # value: [<directory>, "get", ["eprops.txt"]] ? <ehome>.optUnget(eprops) # value: "eprops.txt" In support of the Loader pattern, surgeons also implement the following convenience method: to addLoader(loader, exitName :String) :void { surgeon.addExit(loader, exitName) surgeon.addUncaller(loader) } Devices and Magic PowersBeware! By using files as our first extended example, we may have led you into one of the most common blindspots in thinking about security -- that it is about access to files. Access control is not about which processes have access to which files, but about which objects have access to which objects. Files are just a particularly degenerate passive form of object. Rather, for any subgraph which we don't fully traverse, these issues come up at all exit points -- all places we cut off the traversal during serialization, leaving behind an exit point to reconnect during unserialization. All are potential manipulations of authority. But could we cut the graph anywhere? Could we decide not to cut the graph, but instead serialize the whole thing, including the authority-conveying objects? No. We can divide up all objects into primitives and composites. The composites are defined (or definable) by composition of primitives and other composites. If they are willing, they can be serialized by divulging such a composition as their portrayal. If they are not willing -- if, like the pile__uriGetter, they are opaque to us, then they are virtually primitive -- we cannot tell that they aren't actually primitive.
Whether we can tell or not from within the system, the system itself bottoms out in actual primitives, of which there are two kinds. We already have the pure data primitives, the scalars, like the number 3. There are fixed set of kinds of scalars, which our recognizers and builders already handle as a special case. Finally, there are the primitive soft devices, like the underlying file__uriGetter, which are the only means for affecting the world outside the vat, or being affected by it. A vat without soft devices is as fully isolated and useless as a machine without hard devices. Soft devices cannot be serialized, because serialization produces a depiction, which is just bits, and no mere bit pattern is adequate to grant access to that device on unserialization. In mapping the Java I/O library into E soft devices, it would have been most natural to treat each instance of java.lang.File as a primitive device. Instead, for any such system of related devices, we create one, or a small number, of truly primitive devices, like the file__uriGetter, and then define individual files as derived from these. These root authorities are generally very powerful; we call them magic powers. They convey all the power that would be conveyed in aggregate by all the devices that could be derived from them. Each individual composite device derived from a magic power acts as if it retains access to this magic power (as the directories and normalPiles of the pile system actually do), and generally represents a diminishing of the authority of this power (as the above version of the pile system still only pretends to do). Magic powers should usually follow the Loader pattern, and most of E's already do. Module LoadersOne crucial category of Loaders are the module loaders, one of which is already preinstalled in the default uncallers list, unscope, and scope -- the import__uriGetter. (Note: this is no more than a default. It is not mandated.) E's module loaders are inspired by Java's ClassLoader. ? def capitals := ["New York" => "Albany", > "California" => "Sacramento"] # value: ["New York" => "Albany", "California" => "Sacramento"] ? surgeon.serialize(capitals) # value: "de: <import:org.erights.e.elib.tables.makeConstMap>.fromColumns( # [\"New York\", \"California\"], # [\"Albany\", \"Sacramento\"] # )" ? <import>.optUncall(<elib:tables.makeConstMap>) # value: [<import>, "get", ["org.erights.e.elib.tables.makeConstMap"]] ? surgeon.addLoader(<elib>, "elib__uriGetter") ? def depiction := surgeon.serialize(capitals) # value: "de: <elib:tables.makeConstMap>.fromColumns(\ # [\"New York\", \"California\"], # [\"Albany\", \"Sacramento\"])" ? surgeon.unserialize(depiction) # value: ["New York" => "Albany", "California" => "Sacramento"] Java's ClassLoader is an important invention for language-based security, but not for the reason normally cited. Being an object-capability language, E has no use for the principle-id tagging or stack introspection normally associated with the ClassLoader. Rather, the ClassLoader system allows modules to import each other by name, while at the same time avoiding the creation of any global name space. A Class or Type identity in Java is a ClassLoader identity + a fully qualified name relative to that ClassLoader. A ClassLoader has no name in some yet more global name space. Rather, identity bottoms out in ClassLoader identity, which is exactly object identity -- first class, anonymous, selfish, and opaque. Like the policy provided by E's built in module loaders, JOSS serializes a class by serializing a bit string that acts like the "name" of the class (the serialVersionUID), rather than serializing the behavior definition itself (the .class file). Both therefore treat all behavior definitions in effect as named exit points, to be reconnected on unserialization to a behavior that one hopes is adequately similar. Unlike JOSS, E maintains the ClassLoader's philosophy -- a behavior definition is derived from a loader, and is only named relative to that loader. When serializing a graph of instances of behaviors obtained from several loaders, the unscope and scope must generally hold these loaders, in which case the behaviors in the reconstituted graph maintain the original's separation of loaders. Corresponding Concepts in Conventional SerializationJOSS does not provide named exit points, except for the special case where it mandates them -- serializing classes by serialVersionUID or fully qualified class name rather than by copying and reloading class files. JOSS does have the critical enabling property for implementing the Gordian Surgeon -- an object's portrayal is in terms of other objects, so an object cannot portray itself as having access it doesn't. The resulting depiction represents only the connectivity demonstrated by these portrayals. (*** explain how GraphParams implement the Gordian Surgeon technique in JOSS.) |
||||||||||||
Unless stated otherwise, all text on this page which is either unattributed or by Mark S. Miller is hereby placed in the public domain.
|