github.com/arnodel/golua@v0.0.0-20230215163904-e0b5347eaaa1/quotas.md (about) 1 # Safe Execution Environments (alpha) 2 3 - [Safe Execution Environments (alpha)](#safe-execution-environments-alpha) 4 - [Overview](#overview) 5 - [Meaning of limiting CPU](#meaning-of-limiting-cpu) 6 - [Meaning of limiting memory](#meaning-of-limiting-memory) 7 - [Other restrictions](#other-restrictions) 8 - [Safe Execution Interface](#safe-execution-interface) 9 - [In the standalone golua interpreter](#in-the-standalone-golua-interpreter) 10 - [Within a Lua program](#within-a-lua-program) 11 - [`runtime.context()`](#runtimecontext) 12 - [`runtime.callcontext(ctxdef, f, [arg1, ...])`](#runtimecallcontextctxdef-f-arg1-) 13 - [`runtime.killcontext()`](#runtimekillcontext) 14 - [`runtime.contextdue()`](#runtimecontextdue) 15 - [`runtime.stopcontext()`](#runtimestopcontext) 16 - [When embedding a runtime in Go](#when-embedding-a-runtime-in-go) 17 - [`(*Runtime).PushContext(RuntimeContextDef)`](#runtimepushcontextruntimecontextdef) 18 - [`(*Runtime).PopContext() RuntimeContext`](#runtimepopcontext-runtimecontext) 19 - [`(*Runtime).CallContext(def RuntimeContextDef, f func() *Error) (RuntimeContext, *Error)`](#runtimecallcontextdef-runtimecontextdef-f-func-error-runtimecontext-error) 20 - [`(*Runtime).TerminateContext(format string, args ...interface{})`](#runtimeterminatecontextformat-string-args-interface) 21 - [Finalizers and runtime contexts](#finalizers-and-runtime-contexts) 22 - [How to implement the safe execution environment](#how-to-implement-the-safe-execution-environment) 23 - [CPU limits](#cpu-limits) 24 - [`(*Runtime).RequireCPU(n uint64)`](#runtimerequirecpun-uint64) 25 - [Memory limits](#memory-limits) 26 - [`(*Runtime).RequireMem(n uint64)`](#runtimerequirememn-uint64) 27 - [`(*Runtime).ReleaseMem(n uint64)`](#runtimereleasememn-uint64) 28 - [`(*Runtime).RequireBytes(n int) uint64`](#runtimerequirebytesn-int-uint64) 29 - [`(*Runtime).RequireSize(sz uintptr) uint64`](#runtimerequiresizesz-uintptr-uint64) 30 - [`(*Runtime).RequireArrSize(sz uintptr, n int) uint64`](#runtimerequirearrsizesz-uintptr-n-int-uint64) 31 - [`(*Runtime).ReleaseBytes(n int)`](#runtimereleasebytesn-int) 32 - [`(*Runtime).ReleaseSize(sz uintptr)`](#runtimereleasesizesz-uintptr) 33 - [`(*Runtime).ReleaseArrSize(sz uintptr, n int)`](#runtimereleasearrsizesz-uintptr-n-int) 34 - [Restricting access to Go functions.](#restricting-access-to-go-functions) 35 - [`ComplianceFlags`](#complianceflags) 36 - [`(*GoFunction).SolemnlyDeclareCompliance(ComplianceFlags)`](#gofunctionsolemnlydeclarecompliancecomplianceflags) 37 ## Overview 38 39 First of all: everything in this document is subject to change! 40 41 It can be useful to be able to run untrusted code safely. This is why Golua 42 allows code to be run in a restricted execution environment. This means the following: 43 - the "amount of CPU" available to the code can be limited 44 - the "amount of memory" available to the code can be limited 45 - file IO can be disabled 46 - unsafe Go functions accessible via modules can be disabled 47 48 ### Meaning of limiting CPU 49 By "amount of CPU" we mean this: the Golua VM periodically emits ticks during 50 execution. Not all ticks correspond to the same number of CPU cycles but it is 51 guaranteed that there is an upper bound to the number of CPU cycles occurring 52 between two ticks. 53 54 Limiting the amount of CPU means declaring that the number of 55 ticks shouldn't exceed a certain number. 56 57 The program is required to terminate before the limit is reached. 58 59 ### Meaning of limiting memory 60 61 By "amount of memory" we mean roughly 62 - the number of bytes that are allocated on the heap 63 - the size of the "stack frames" associated with Lua functions and Go functions 64 called from Golua. 65 66 Memory used can be counted down when it is known that an object is no longer 67 going to be used (e.g. a Lua function "stack frame"), but in many cases this 68 does not happen. So counting memory used works a bit as if GC was mostly turned 69 off. 70 71 Below is an example that currently would run have memory counted as used 72 increasing linearly in terms of `n`. 73 ```lua 74 for i = 1, n do 75 -- The following creates a new table, consuming memory. That table will get 76 -- GCed shortly but that won't make the amount of memory go down. 77 t = {} 78 end 79 ``` 80 81 Limiting the amount of memory means declaring that the "amount of memory" used 82 as defined above shouldn't exceed a certain number. 83 84 The program is required to terminate before the limit is reached. 85 86 ### Other restrictions 87 88 When these restricitions are in place, trying to call a function that perform IO 89 access (or runs unsafe) should return an error, but not terminate the program. 90 91 ## Safe Execution Interface 92 93 There are three ways to apply the limits described above. 94 - When creating the Lua runtime from the program embedding Golua 95 - Within a Lua program, to safely execute some Lua code 96 - When starting the standalone `golua` interpreter 97 98 The restrictions are managed via the notion of runtime context, which is an 99 object that accounts for resource limits and resource consumed. A runtime 100 context is associated with the Lua thread of execution (so there is only one 101 such context active at a time). 102 ### In the standalone golua interpreter 103 104 Command line flags allow running the interpreter with restrictions. Here is the 105 relevant extract from `golua -help`: 106 ``` 107 -cpulimit uint 108 CPU limit 109 -memlimit uint 110 memory limit 111 -nogolib 112 disable Go bridge 113 -noio 114 disable file IO 115 ``` 116 117 ### Within a Lua program 118 119 Golua provides a `runtime` library which exposes two functions 120 121 #### `runtime.context()` 122 123 Returns an object `ctx` representing the current context. This object mostly 124 cannot be mutated but gives useful information about the execution context. 125 126 - `ctx.status` is the status of the context as a string, which can be: 127 - `"live"` if this is the currently running context; 128 - `"done"` if this execution context terminated successfully; 129 - `"error"` if this execution context terminated with an error 130 - `"killed"` if the context terminated because it would otherwise have 131 exceeded its limits. 132 - `ctx.kill` returns an object giving the hard resource limits of `ctx`. If 133 any of these limits are reached then the context will be terminated 134 immediately, returning execution to the parent context. Hard limits cannot 135 exceed their parent's hard limits. 136 - `ctx.stop` returns an object giving the resource soft limits of `ctx`. Soft 137 limits cannot exceed hard limits, and by default cannot be increased from the 138 parent's soft limits. In future if there is are clear use-cases for 139 increasing the soft limits from the parent's, another API endpoint can be 140 provided. 141 - `ctx.used` returns an object giving the used resources of `ctx` 142 - `ctx.flags` returns a string describing the flags that any code running in 143 this context has to comply with. Those flags are `"memsafe"`, `"cpusafe"`, 144 `"timesafe"` and `"iosafe"` currently. 145 - `ctx.due` returns true if any of the context's soft limits have been 146 exhausted. 147 148 Additionally there are two methods that allow mutation of the context. 149 150 - `ctx:killnow()` updates the context's state so that its hard limits are 151 considered exhausted. The effect on a running context will be to be 152 terminated immediately. 153 - `ctx:stopnow()` update the context's state so that its soft limits are 154 considered exhausted. The effect is that `ctx.due` returns true. 155 156 #### `runtime.callcontext(ctxdef, f, [arg1, ...])` 157 158 This function creates a new execution context `ctx` from `ctxdef`, calls 159 `f(arg1, ...)` in this context, then returns `ctx`. Additionally 160 - if the call was successful, it also returns the returns values of `f(arg1. 161 ...)`; 162 - if there was a non-terminal error in the call, it also returns the error. In 163 this respect, the `runtime.callcontext()` function always behaves like 164 `pcall`. 165 166 By default `ctx` will inherit from the current context: its CPU and memory 167 limits will be the amount of unused CPU and memory in the current context, and 168 it inherits the `io` and `golib` flags from the current context. 169 170 The argument `ctxdef` allows restricting `ctx` further. It is a table with any 171 of the following attributes. 172 - `kill`: if set, it should be a table. Attributes can be set in this table 173 with names `mem`, `cpu` and values a positive integer. This is used to set 174 the context's hard resource limits. 175 - `stop`: same format as `kill` but describes soft limits. It will be used to 176 set the context's soft resource limits. 177 - `flags`: same format as for a context definition (e.g. `"cpusafe memsafe"`) 178 179 Here is a simple example of using this function in the golua repl: 180 ```lua 181 > ctx = runtime.callcontext({kill={cpu=1000}}, function() while true do end end) 182 > print(ctx) 183 killed 184 > print(ctx.used.cpu, ctx.kill.cpu) 185 999 1000 186 > print(ctx.flags) 187 cpusafe 188 > print(ctx.used.memory, ctx.kill.memory) 189 0 nil 190 ``` 191 192 #### `runtime.killcontext()` 193 194 This function terminates the current context immediately, returning to the 195 parent context. It is as if a hard resource limit had been hit. It can be used 196 when a soft resource limit has been hit and the program decides to stop. 197 198 Alternatively contexts have a method to achieve the same: `ctx:killnow()`. On a 199 context that is not currently running, the effect is to kill it as soon at it is 200 resumed. 201 #### `runtime.contextdue()` 202 203 This function returns true if any of the soft resource limits has been hit on 204 the currently running context. 205 206 Alternatively contexts have a property `ctx.due` that is set to true if the 207 context `ctx` has exhausted any of its soft limits. 208 209 210 #### `runtime.stopcontext()` 211 212 This function updates the current context so that its soft limits are considered 213 exhaused. 214 215 Alternatively contexts have a method to achieve the same: `ctx:stopnow()`. 216 217 ### When embedding a runtime in Go 218 219 There is a `RuntimeContext` interface in the `runtime` package. It is 220 implemented by `*runtime.Runtime` and allows inspection of the current execution 221 context. We will see further down that contexts that are terminated are also 222 available via this interface. 223 224 ```golang 225 type RuntimeContext interface { 226 HardResourceLimits() RuntimeResources 227 SoftResourceLimits() RuntimeResources 228 UsedResources() RuntimeResources 229 230 Status() RuntimeContextStatus 231 Parent() RuntimeContext 232 233 RequiredFlags() ComplianceFlags 234 235 SetStopLevel(StopLevel) 236 Due() bool 237 } 238 ``` 239 240 The `runtime` package also defines a `RuntimeContextDef` type whose purpose is 241 to specify the properties of a new execution context to create. 242 243 ```golang 244 type RuntimeContextDef struct { 245 HardLimits RuntimeResources 246 SoftLimits RuntimeResources 247 RequiredFlags ComplianceFlags 248 MessageHandler Callable 249 } 250 ``` 251 252 As mentioned above, a Lua runtime is of type `*runtime.Runtime` and implements 253 the `RuntimeContext` interface. It also implements two methods. 254 255 #### `(*Runtime).PushContext(RuntimeContextDef)` 256 257 Creates a new context from the definition and makes it the active context. As 258 described in the Lua section, the new context is not allowed to be less 259 restrictive than the one it replaces. 260 261 #### `(*Runtime).PopContext() RuntimeContext` 262 263 Removes the active context from the "context stack" and returns it. It ensures 264 that resources consumed in the popped context will be accounted for in the 265 parent context. 266 267 Here is a simple example of how they could be used. 268 269 ```golang 270 import ( 271 "os" 272 rt "github.com/arnodel/golua/runtime" 273 274 ) 275 276 func main() { 277 r := rt.NewRuntime(os.Stdout) 278 279 r.PushContext(rt.RuntimeContextDef{ 280 HardLimits: rt.RuntimeResources{ 281 Mem: 100000, 282 Cpu: 1000000, 283 }, 284 RequiredFlags: rt.ComplyIoSafe 285 }) 286 // Now executing Lua code in this runtime will be subject to these limitations 287 // If the limits are exceeded, the Go runtime will panic with a 288 // rt.QuotaExceededError. 289 290 // Do something in this context 291 292 ctx := r.PopContext() 293 // We are back to the initial execution context. PushContext calls can be 294 // nested. The returned ctx is a RuntimeContext that can be inspected. 295 } 296 ``` 297 298 The `*runtime.Runtime` type has another method. 299 300 #### `(*Runtime).CallContext(def RuntimeContextDef, f func() *Error) (RuntimeContext, *Error)` 301 302 Similar to Lua's `runtime.callcontext`. It is a convenience function to run 303 some code in a given context, catching the `QuotaExceededError` panics if they 304 occur and returning the finished context. So the above could be rewritten safely 305 as follows. 306 307 ```golang 308 import ( 309 "os" 310 rt "github.com/arnodel/golua/runtime" 311 312 ) 313 314 func main() { 315 r := rt.NewRuntime(os.Stdout) 316 317 ctx, err := r.CallContext(rt.RuntimeContextDef{ 318 HardLimits: rt.RuntimeResources{ 319 Mem: 100000, 320 Cpu: 1000000, 321 }, 322 RequiredFlags: rt.ComplyIoSafe 323 }, func() *rt.Error { 324 // Do something in this context, returning an error if appropriate. 325 // That error will set the context status to "error". 326 }) 327 328 // Panics due to quota exceeded will be recovered from. 329 } 330 ``` 331 332 #### `(*Runtime).TerminateContext(format string, args ...interface{})` 333 334 Terminate the context immediately if it is live. 335 336 ## Finalizers and runtime contexts 337 338 In Lua it is possible to add finalizers to two types of values: tables and 339 userdata. Finalizers are run once the garbage collector knows the values are no 340 longer reachable. This is used in the standard library to make sure open files 341 which are no longer used are closed. 342 343 The Golua runtime makes sure that when a value is created within a runtime 344 context with restricted resources, running its finalizer will not use another 345 context's resources. However, only in the case of userdata, it is sometimes 346 the case the value contains a resource that should be released unconditionnally 347 (e.g. a file descriptor). Golua provides a general mechanism to support that, 348 simply by defining a `ReleaseResources` method on the underlying type. That 349 method is guaranteed to run before the runtime context is closed, but after the 350 Lua finalizer runs if it exists. For example in the standard library, the 351 underlying type of file userdata is as follows. 352 353 ```golang 354 type File struct { 355 file *os.File 356 status fileStatus 357 reader bufReader 358 writer bufWriter 359 } 360 ``` 361 362 When file userdata becomes unreachable, the underlying `File` instance should 363 close the `os.File` instance it owns. This is done as follows. 364 365 ```golang 366 367 // The *File type implements the ResourceReleaser interface. 368 func (f *File) ReleaseResources(d *rt.UserData) { 369 f.cleanup() 370 } 371 372 // Included to show what happens in Prefinalize 373 func (f *File) cleanup() { 374 if !f.IsClosed() { 375 f.Close() 376 } 377 if f.IsTemp() { 378 _ = os.Remove(f.Name()) 379 } 380 } 381 ``` 382 383 The runtime makes sure that any userdata that implements 384 `runtime.ResourceReleaser` interface will have its `ReleaseResources` method 385 called unconditionally. Of course it is important that the code in those 386 methods consumes as little resources as possible. 387 388 For details about the semantics see the 389 [userdata.quotas.lua](runtime/lua/userdata.quotas.lua) test file 390 391 ## How to implement the safe execution environment 392 393 ### CPU limits 394 395 The basic means of enforcing CPU limits is the following. 396 #### `(*Runtime).RequireCPU(n uint64)` 397 398 This method checks that `n` units of CPU are available. If that is the case, 399 the amount of CPU used is updated and execution continues. Otherwise, the Go 400 thread panics with `runtime.QuotaExceededError`. 401 402 The approach is to call `RequireCPU` before a unit of work is done. 403 - In a loop an amount of CPU should be required that is proportional to the 404 number of iterations. 405 - Nested Go function calls should require CPU proportional to the depth of the 406 nested calls. 407 - When running code in third party packages (including the Go Standard Library) 408 it should be possible to obtain and upper bound to the amount of CPU required 409 ahead of the call and require it. If the third party function is given a 410 callback it may be possible to use that (e.g. `sort.Sort`). 411 412 ### Memory limits 413 414 The basic means of enforcing memory limits are the following. 415 416 #### `(*Runtime).RequireMem(n uint64)` 417 418 This methods checks that `n` units of memory are available. If that is the case, 419 the amount of CPU used is updated and execution continues. Otherwise, the Go 420 thread panics with `runtime.QuotaExceededError`. 421 422 #### `(*Runtime).ReleaseMem(n uint64)` 423 424 This methods reduces the amount of memory used by `n` units (if possible). It 425 is generally not used but can be useful in some cases (e.g. when a big temporary 426 object needs to be allocated). 427 428 Often we know how much memory is required in terms of bytes or size of data 429 structures, so there are some convenience methods to address that. 430 431 #### `(*Runtime).RequireBytes(n int) uint64` 432 433 Require enough memory to store `n` bytes. Return the number of memory units 434 required. 435 436 #### `(*Runtime).RequireSize(sz uintptr) uint64` 437 438 Require enough memory to store an obect of size `sz`, size as returned by 439 `unsafe.Sizeof()`. Return the number of memory units required. 440 441 #### `(*Runtime).RequireArrSize(sz uintptr, n int) uint64` 442 443 Require enough memory to store `n` objects of size `sz`, e.g. a slice or an 444 array of objects. Return the number of memory units required. 445 446 447 There are corresponding methods for releasing memory 448 449 #### `(*Runtime).ReleaseBytes(n int)` 450 451 #### `(*Runtime).ReleaseSize(sz uintptr)` 452 453 #### `(*Runtime).ReleaseArrSize(sz uintptr, n int)` 454 455 The approach is to call `RequireMem` or one of the derived method before some 456 memory allocation. Memory allocation occurs when 457 - A new string is created 458 - A new table is created 459 - A new item is inserted into a table 460 - A new Lua closure is created 461 - A new Lua continuation is created (that is akin to a "Lua call frame") 462 - A new Go function is created 463 - A new UserData instance is created 464 - Buffered IO occurs 465 - Lua source code is compiled 466 467 Moreover it may be that calling a function in the standard library can cause 468 memory allocations. 469 470 In some case it may be appropriate to return memory. An example is when a Lua 471 continuation ends. Returning its memory allows tail-calls to have the same 472 memory footprint as loops. 473 474 ### Restricting access to Go functions. 475 476 There is a built-in mechanism for making sure that Go function called in the Lua 477 runtime comply with the safe execution environment requirements. As there are 478 different levels of compliance, a number of Compliance Flags can be defined. 479 Any of those can be required in an execution context, and only Go functions 480 which have been declared explicitly as implementing these compliance flags will 481 be allowed to be run. 482 483 This approach has several advantages 484 - Granularity: for each Go function it is required to define what compliance 485 flags it implements. So a single Lua module could include Go functions with 486 different compliance profiles. 487 - Future proof: if new compliance flags are added, existing functions will not 488 comply with those by default, so it limits the risk of misuse. On the other 489 hand an existing function will still be able to be used in an environment not 490 requiring the new compliance flags. 491 - Safety: It is safer than controlling access to modules via a 492 blocklist/allowlist. As Lua's runtime is very dynamic, it would probably be 493 easy to circumvent such measures. 494 495 #### `ComplianceFlags` 496 497 The runtime defines a number of compliance flags, currently: 498 499 ```golang 500 501 type ComplianceFlags uint16 502 503 const ( 504 // Only execute code checks memory availability before allocating memory 505 ComplyMemSafe ComplianceFlags = 1 << iota 506 507 // Only execute code that checks cpu availability before executing a 508 // computation. 509 ComplyCpuSafe 510 511 // Only execute code that complies with IO restrictions (currently only 512 // functions that do no IO comply with this) 513 ComplyIoSafe 514 ) 515 ``` 516 517 #### `(*GoFunction).SolemnlyDeclareCompliance(ComplianceFlags)` 518 519 Any Go functions that can be called from Lua is wrapped in an instance of 520 `*rt.GoFunction`. By default this instances does not include any compliance 521 flags. It is possible to declare compliance with 522 `(*GoFunction).SolemnlyDeclareCompliance()` 523 524 Before execution, the current context's `RequiredFlags` value is checked against 525 the compliance flags declared by the Go functions. If any of the required flags 526 is not complied with by the function, execution will immediately return an error 527 (but not terminate the context).