~aleteoryx/tclircc

1c9eee37d59cadc5728509594b0fc666f3fa510c — aleteoryx a month ago 592f2b1
manifest loading basics
3 files changed, 84 insertions(+), 3 deletions(-)

M main.tcl
M plugins.tcl
A testplugin/manifest.tcl
M main.tcl => main.tcl +2 -0
@@ 43,6 43,8 @@ source plugins.tcl
start_thread irc
start_thread ui

plugins::load "$path[file separator]testplugin"

${log}::debug "opening initial window..."
thread::send [t::ns tclircc::ui] {mk_toplevel name; return $name} initial
${log}::debug "initial window opened: $initial"

M plugins.tcl => plugins.tcl +79 -3
@@ 1,16 1,92 @@
package require sha256

# plugin manifest format:
# tcl script that must return the following
# tcl script that must set the following variables at a minimum:
# - name: string
# - slug: string
#   must match /^[-_a-zA-Z0-9]+$/
# - version: string[]
#   ordered like {1} > {0}, {0 1} < {1 0}. {1 1} > {1}
# may set:
# - description: string
# - author: string[]
#   should contain a list of names with optional website or email, e.g.
#   {{Aleteoryx <https://aleteoryx.me>}
#    {Alice P Hacker <aph@example.com>}}
# - license: string
#   may begin with spdx: to indicate an spdx id or file: to indicate
#   a file path relative to the plugin root. .. and root references are
#   stripped. if neither prefix applies, assumed to be literal license
#   text.


namespace eval plugins {
  variable plugins
  variable log [logger::init tclircc::plugins]

  proc load {dir} {
    set mf_fd [open "$dir[file separator]manifest.tcl"]
    variable log

    ${log}::info "attempting to load plugin from \"$dir\"..."

    set mf_path "$dir[file separator]manifest.tcl"
    if {!([file isfile $mf_path] && [file readable $mf_path])} {
      ${log}::error "couldn't load plugin: manifest \"$mf_path\" not present or not readable!"
      return 0
    }
    set mf_fd [open $mf_path]
    set manifest [read $mf_fd]
    close $mf_fd

    
    interp create -safe mf_exec
    # prevent escapes
    interp hide mf_exec package
    # prevent hanging the thread
    interp hide mf_exec vwait
    interp hide mf_exec after
    interp hide mf_exec update
    interp hide mf_exec gets
    interp hide mf_exec read

    # this is more than enough for any reasonable manifest
    interp limit mf_exec time -seconds [expr {[clock seconds] + 1}]
    interp limit mf_exec command -value 10000

    # untrusted code time
    ${log}::debug "evaluating manifest..."
    set untrusted_result [catch {
      set mf_result [catch { interp eval mf_exec $manifest } result opts]
      if {$mf_result != 0} {
        ${log}::error "couldn't load plugin: manifest return code $mf_result: $result"
        return 0
      }

      set has_required [interp eval mf_exec {expr {[info exists name] &&
                                                   [info exists slug] &&
                                                   [info exists version]}}]
      if {!$has_required} {
        ${log}::error "couldn't load plugin: missing required values in manifest"
        return 0
      }

      set name [interp eval mf_exec {set name}]
      set slug [interp eval mf_exec {set slug}]
      set version [interp eval mf_exec {set version}]

      ${log}::debug "manifest loaded: $name ($slug) v[join $version .]"
    } result opts]
    switch -- $untrusted_result {
      0 { return result } 2 { return result }
      1 {
        ${log}::error "couldn't load plugin: $result"
        return 0
      }
      default {
        ${log}::alert "unexpected return code from plugin handling: $untrusted_result"
        ${log}::alert "return options: $opts"
        ${log}::alert "THIS MAY INDICATE A SANDBOX COMPROMISE!"
        return 0
      }
    }
  }
}

A testplugin/manifest.tcl => testplugin/manifest.tcl +3 -0
@@ 0,0 1,3 @@
set name "test plugin"
set slug "test"
set version {0 0 1}