summaryrefslogtreecommitdiff
path: root/notes/design
blob: 3cd1c905bced734fc8e474d070348553a0eec1b0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
Sandbox (for) Untrusted Procedure Partitioning (in) Lua Engine - Supple
=======================================================================

# Requirements (mostly via Gitano)

* Be able to run complex hook scripts in a totally restricted sandbox.
* Be able to access appropriately filtered objects/functions/data from the
  caller in some manner
* Have the caller able to call back into the sandbox
* Have the sandbox be in a subprocess which can be ulimit()ed etc.
* Perhaps have some way to ensure the subprocess cannot access any of the
  filesystem.  (chrooted?)
* Allow the caller to inject functions, values, etc into the remote sandbox
  before execution.  Perhaps even modules etc.
* Be a minimal overhead system but still be 100% policy controllable at the
  caller side of the sandbox
* May as well use Luxio for the low level operations.
* Provide the code to execute over the sandbox connection after the sandbox
  has locked itself down.

# Possible implementation details

* Serialise requests and responses as simple packets which can be punted
  across a single FD pair
* Perhaps use userdata to ensure that there's no chance of rawset() etc
  happening on either end for 'remote' objects.
* Cannot use coroutines since pure lua 5.1 cannot yield across metamethod
  boundaries :-(  As such, pcall()s everydamnedwhere :-)
* Types of packets passing across the link
  * Procedure call
  * Procedure reply

## Context objects

Contextual objects are anything non-integral (i.e. a table or userdata) which
is passed across the connections.  They get transformed into contextual objects
which are always represented on the remote end as a userdata which has
appropriate metamethods to cope with the object on the far end.  Only
metamethods which are defined by the sending end (and always __index and
__newindex if it's a table) are defined on the receiving end in order to
increase realism.  The *sandboxed* end of the connection has pairs() and
ipairs() augmented to cope with contextual objects, the non-sandboxed end has
no such help.

Contextual objects also represent remote functions (although they always
tostring() to "function" rather than "table" or "userdata") and only support
__call.

## Notification of contextual object

Contextual objects are always transferred as tables with the following keys:

* type:  "table" "userdata" "function"
* tag:   A tag for the caller to use when referring to this object in future
* methods: Optional table containing the set of metamethods defined on this
  object.

Note that the remote end will always augment the methods list with __gc so that
the notification of GC can be made.  Also if the type is "table" then __index
and __newindex metamethods will be added for those normal table operations.  If
the type is "function" then __call will also be forcibly added.

At either end, the set of contextual objects is held in tables.

* my_objects -- objects sent from this side to the other which have not been
  garbage collected on the remote end.  Strong key (object) strong value
  (tag)
* their_objects -- objects from the remote side strong key (tag) weak value
  (object)

Also, the objects created as part of a function call are held strongly by the
receiver of the procedure call so that they *cannot* go out of scope until
the procedure call has sent its return value to the caller.

## Procedure call

Procedure calls are quite simple, they consist of a contextual object and a
procedure name and a set of arguments.

The procedure names are always of the form of metamethod names.

The __gc metamethod tells the remote end that the local end is 'forgetting' the
contextual object and the remote end can drop it from its cache.

Thusly a procedure call (unserialised) looks approximately like this:

    {
        object = "some object tag",
        method = "__something",
        args = {
            n = 4, -- The number of arguments.  Trailing nils might be lost.
            "string arg",
            1234,  -- number argument
	    false, -- boolean argument
            { -- Object argument of some kind
	        type = "table",
                tag = "tag name",
		methods = { "__call" }
            }
        },
    }

If the call is passing a contextual object from the remote end back again then
it omits the type and methods values, only passing the tag.  Thusly whenever a
contextual object is passed as an argument, it is automatically unwrapped at
the remote end into the local object.  This means that we do not end up with
multiple layers of contextual objects going back and forth.

## Procedure returns

Procedure returns are always of the following form:

    {
        error = true,
        message = "some message",
        traceback = "some traceback"
    }

or

    {
        error = false,
        results = {
            n = N, -- The number of results detected
            -- Any returns as a numerically indexed table.
            -- Note that trailing nils may be collapsed.
        }
    }

If the procedure return is an error form then the caller then stashes the
traceback in the right place and raises a Lua error on the caller's side.  This
process might bounce back and forth until something catches the error in full.

Otherwise, the results are returned exactly as arguments were provided,
i.e. strings, booleans and numbers are passed directly, and anything else is an
object.  If a brand new object is returned as part of the call then the
lifetime of that object will very much be limited to the caller remembering to
anchor it somewhere.  Otherwise it's quite possible that during the return from
the procedure call, a __gc call will be automatically invoked to clean up.

# Serialising requests and replies

All requests and replies are structured as Lua tables once deserialised.  The
serialised form of the messages is simply a string which can be loaded and run
to return the values.  The limited number of formats mean that the serialisers
can be written explicitly and the deserialiser can simply be a generic loader
followed by a series of asserts and measures to ensure nothing malicious gets
injected.