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 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 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.