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