package require sha256 # plugin manifest format: # 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 } # {Alice P Hacker }} # - 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} { 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 { if [catch { interp eval mf_exec $manifest } result opts] { ${log}::error "couldn't load plugin: return code [dict get $opts -errorcode]: $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 } } } }