aboutsummaryrefslogtreecommitdiffhomepage
path: root/notes/capabilities.txt
blob: 2d359d082b9bce78a322720822ee32580e808fb8 (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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167

Capabilities
============
Arenas will be used for capabilities.

Capability categories
----------------------
There can be a lot of capabilities, and more could be added over time.

Add categories to solve this. E.g. "file", "file_read", "net", "net_dns",
"net_http", etc.

TODO decide on a base set of categories.

It would be nice if custom categories are possible also.
(i.e. that one can use categories from dependencies)

There could be capability categories that take parameters, e.g.
filenames, domain names, IP addresses, port numbers, etc.

Temporary capability dropping
-----------------------------
It should be possible to temporarily drop capabilities in the current
arena, without creating a sub-arena.

Perhaps something like this:

    func do_stuff(arena ar)
    {
        restrict ar allowlist .file_read {
            ref Png = .load(ar, "file.png")
        }
        # XXX but what if the .load method leaves some pending "actions"
        # in the arena, that trigger disallowed operations AFTER the
        # end of the restrict block?
    }

    func arena Thing.do_things()
    {
        restrict this {   # nothing is allowed except for allocation
            this.compute_stuff()
        }
    }

Note that "goto", "break", "continue" and "return" must be either
restricted, or carefully handled. Otherwise, restrictions could be
bypassed, or worse, capability state could be corrupted.
- goto to the inside of the restrict block has to either
  initialize the restrictions, or it could be forbidden,
- goto/break/continue/return out of the restrict block has to either
  restore the state, or it could be forbidden.

Overriding/proxying capabilities
--------------------------------
It needs to be possible to override/proxy functionality such as IO.
This can be used to redirect filesystem access to an in-memory filesystem
for example.

Note that overriding some functionality, but not certain others, might be
a sign of an error. E.g. overriding open() but not openat() or creat()
(to use C functions as an example; those don't exist in SLUL obviously).
So in that case, the override function should either make the other functions
trap, or make them call the overridden function (if that is safe to do).

Early detection of missing capabilities
---------------------------------------

This is tricky. It could be implemented in many ways:
* By calling a "probe function" in each function taking an arena parameter,
  that recursively checks that all edge cases are supported by the arena.
  This will be slow.
* By calling a "probe function" when creating the arena. This is better,
  and probably much faster.
* By relying on the loader (or runtime) to be able to map functions to
  required capabilities, and using this functionality when a restricted
  arena is created.
* By having the compiler do a best-effort scan. This would either require
  annotations, or the compiler would have to fetch all deps recursively :(

Capabilities with parameters
----------------------------
Many/most operations (e.g. open(), mkdir(), etc.) will take different
parameters and return values. So any filter/handler functions need to
be able to have different parameters and return values
(perhaps the result could go into a parameter).

    func filtered_mkdir(arena Operation<OpParamsMkDir> op)
    {
        if op.params.filename.starts_with("/srv") {
            op.proceed()
        } else {
            op.block()
        }
    }

Or perhaps use a special syntax:

    filter restrict_to_path {
        params {
            string allowed_path
        }
        action string_to_ospath {
            op.require(this.filepath.starts_with(params.allowed_path))
            op.proceed()
        }
    }

    func do_stuff(arena ar)
    {
        restrict ar filter restrict_to_path(.allowed_path = "/srv") {
            ref Png = .load(ar, "file.png")
        }
    }

Or:

    type RestrictToPath = struct {
        string allowed_path
    }

    #action RestrictToPath.string_to_ospath {
    func RestrictToPath.string_to_ospath(arena Operation<StringToOsPath> op) {
        op.require(op.filepath.starts_with(this.allowed_path))
        op.proceed()
    }

How to store arena capabilities?
--------------------------------

Goals:
* It should be fast (avoid indirections etc.)
* It should not use too much memory if there are lots of arenas.
* It should if possible avoid modifiable function pointers
  (they can be used as gadgets in security exploits)


Method 1: Have a function pointer array in the base struct of the arena.
* What should the array indices be? I.e. how to map a function to a number
  when there are multiple libraries loaded at load time (or even at runtime).
* An array that maps between IDs and function pointers could be built
  at load-time, and then made read-only.

Method 2: Have a "filter chain".
* The arena base struct has an array of function pointers to
  "handler" functions.
* A capability-controlled function call starts by calling the topmost
  (most recent / most specific) handler function.
* The handler function checks which capability-function is requested
  and can chose to:
    - Perform the operation (native code only)
    - Emulate the operation
    - Do nothing
    - Return an error code
    - Trap
    - Call the next handler function (e.g. if it does not know how to
      handle it).
    - A selection/combination of the above (e.g. allow only a certain
      function to be called and only with a certain set of parameters).

Method 3: Have a BPF-like solution.
* The arena base struct could contain bytecode.
* How can it call SLUL functions? The interpreter would be part of the
  runtime, so it would have to somehow know about the loaded libraries
  and the functions in them. Or, the bytecode could contain function
  pointers.

Method 3: Have some kind of map structure
* E.g. from (module-id, capability-id) to something that describes what to do.