# safeinit.tcl -- # # This code runs in a master to manage a safe slave with Safe Tcl. # See the safe.n man page for details. # # Copyright (c) 1996-1997 Sun Microsystems, Inc. # # See the file "license.terms" for information on usage and redistribution # of this file, and for a DISCLAIMER OF ALL WARRANTIES. # # SCCS: @(#) safeinit.tcl 1.38 97/06/20 12:57:39 # This procedure creates a safe slave, initializes it with the # safe base and installs the aliases for the security policy mechanism. proc tcl_safeCreateInterp {slave} { global auto_path # Create the slave. interp create -safe $slave # Set its auto_path interp eval $slave [list set auto_path $auto_path] # And initialize it. return [tcl_safeInitInterp $slave] } # This procedure applies the initializations to an already existing # interpreter. It is useful when you want to enable an interpreter # created with "interp create -safe" to use security policies. proc tcl_safeInitInterp {slave} { upvar #0 tclSafe$slave state global tcl_library tk_library auto_path tcl_platform # These aliases let the slave load files to define new commands interp alias $slave source {} tclSafeAliasSource $slave interp alias $slave load {} tclSafeAliasLoad $slave # This alias lets the slave have access to a subset of the 'file' # command functionality. tclAliasSubset $slave file file dir.* join root.* ext.* tail \ path.* split # This alias interposes on the 'exit' command and cleanly terminates # the slave. interp alias $slave exit {} tcl_safeDeleteInterp $slave # Source init.tcl into the slave, to get auto_load and other # procedures defined: if {$tcl_platform(platform) == "macintosh"} { if {[catch {interp eval $slave [list source -rsrc Init]}]} { if {[catch {interp eval $slave \ [list source [file join $tcl_library init.tcl]]}]} { error "can't source init.tcl into slave $slave" } } } else { if {[catch {interp eval $slave \ [list source [file join $tcl_library init.tcl]]}]} { error "can't source init.tcl into slave $slave" } } # Loading packages into slaves is handled by their master. # This is overloaded to deal with regular packages and security policies interp alias $slave tclPkgUnknown {} tclSafeAliasPkgUnknown $slave interp eval $slave {package unknown tclPkgUnknown} # We need a helper procedure to define a $dir variable and then # do a source of the pkgIndex.tcl file interp eval $slave \ [list proc tclPkgSource {dir args} { if {[llength $args] == 2} { source [lindex $args 0] [lindex $args 1] } else { source [lindex $args 0] } }] # Let the slave inherit a few variables foreach varName \ {tcl_library tcl_version tcl_patchLevel \ tcl_platform(platform) auto_path} { upvar #0 $varName var interp eval $slave [list set $varName $var] } # Other variables are predefined with set values foreach {varName value} { auto_noexec 1 errorCode {} errorInfo {} env() {} argv0 {} argv {} argc 0 tcl_interactive 0 } { interp eval $slave [list set $varName $value] } # If auto_path is not set in the slave, set it to empty so it has # a value and exists. Otherwise auto_loading and package require # will complain. interp eval $slave { if {![info exists auto_path]} { set auto_path {} } } # If we have Tk, make the slave have the same library as us: if {[info exists tk_library]} { interp eval $slave [list set tk_library $tk_library] } # Stub out auto-exec mechanism in slave interp eval $slave [list proc auto_execok {name} {return {}}] return $slave } # This procedure deletes a safe slave managed by Safe Tcl and # cleans up associated state: proc tcl_safeDeleteInterp {slave args} { upvar #0 tclSafe$slave state # If the slave has a policy loaded, clean it up now. if {[info exists state(policyLoaded)]} { set policy $state(policyLoaded) set proc ${policy}_PolicyCleanup if {[string compare [info proc $proc] $proc] == 0} { $proc $slave } } # Discard the global array of state associated with the slave, and # delete the interpreter. catch {unset state} catch {interp delete $slave} return } # This procedure computes the global security policy search path. proc tclSafeComputePolicyPath {} { global auto_path tclSafeAutoPathComputed tclSafePolicyPath set recompute 0 if {(![info exists tclSafePolicyPath]) || ("$tclSafePolicyPath" == "")} { set tclSafePolicyPath "" set tclSafeAutoPathComputed "" set recompute 1 } if {"$tclSafeAutoPathComputed" != "$auto_path"} { set recompute 1 set tclSafeAutoPathComputed $auto_path } if {$recompute == 1} { set tclSafePolicyPath "" foreach i $auto_path { lappend tclSafePolicyPath [file join $i policies] } } return $tclSafePolicyPath } # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # tclSafeAliasSource is the target of the "source" alias in safe interpreters. proc tclSafeAliasSource {slave args} { global auto_path errorCode errorInfo if {[llength $args] == 2} { if {[string compare "-rsrc" [lindex $args 0]] != 0} { return -code error "incorrect arguments to source" } if {[catch {interp invokehidden $slave source -rsrc [lindex $args 1]} \ msg]} { return -code error $msg } } else { set file [lindex $args 0] if {[catch {tclFileInPath $file $auto_path $slave} msg]} { return -code error "permission denied" } set errorInfo "" if {[catch {interp invokehidden $slave source $file} msg]} { return -code error $msg } } return $msg } # tclSafeAliasLoad is the target of the "load" alias in safe interpreters. proc tclSafeAliasLoad {slave file args} { global auto_path if {[llength $args] == 2} { # Trying to load into another interpreter # Allow this for a child of the slave, or itself set other [lindex $args 1] foreach x $slave y $other { if {[string length $x] == 0} { break } elseif {[string compare $x $y] != 0} { return -code error "permission denied" } } set slave $other } if {[string length $file] && \ [catch {tclFileInPath $file $auto_path $slave} msg]} { return -code error "permission denied" } if {[catch { switch [llength $args] { 0 { interp invokehidden $slave load $file } 1 - 2 { interp invokehidden $slave load $file [lindex $args 0] } default { error "too many arguments to load" } } } msg]} { return -code error $msg } return $msg } # tclFileInPath raises an error if the file is not found in # the list of directories contained in path. proc tclFileInPath {file path slave} { set realcheckpath [tclSafeCheckAutoPath $path $slave] set pwd [pwd] if {[file isdirectory $file]} { error "$file: not found" } set parent [file dirname $file] if {[catch {cd $parent} msg]} { error "$file: not found" } set realfilepath [file split [pwd]] foreach dir $realcheckpath { set match 1 foreach a [file split $dir] b $realfilepath { if {[string length $a] == 0} { break } elseif {[string compare $a $b] != 0} { set match 0 break } } if {$match} { cd $pwd return 1 } } cd $pwd error "$file: not found" } # This procedure computes our expanded copy of the path, as needed. # It returns the path after expanding out all aliases. proc tclSafeCheckAutoPath {path slave} { global auto_path upvar #0 tclSafe$slave state if {![info exists state(expanded_auto_path)]} { # Compute for the first time: set state(cached_auto_path) $path } elseif {"$state(cached_auto_path)" != "$path"} { # The value of our path changed, so recompute: set state(cached_auto_path) $path } else { # No change: no need to recompute. return $state(expanded_auto_path) } set pwd [pwd] set state(expanded_auto_path) "" foreach dir $state(cached_auto_path) { if {![catch {cd $dir}]} { lappend state(expanded_auto_path) [pwd] } } cd $pwd return $state(expanded_auto_path) } proc tclSafeAliasPkgUnknown {slave package version {exact {}}} { tclSafeLoadPkg $slave $package $version $exact } proc tclSafeLoadPkg {slave package version exact} { if {[string length $version] == 0} { set version 1.0 } tclSafeLoadPkgInternal $slave $package $version $exact 0 } proc tclSafeLoadPkgInternal {slave package version exact round} { global auto_path upvar #0 tclSafe$slave state # Search the policy path again; it might have changed in the meantime. if {$round == 1} { tclSafeResearchPolicyPath if {[tclSafeLoadPolicy $slave $package $version]} { return } } # Try to load as a policy. if [tclSafeLoadPolicy $slave $package $version] { return } # The package is not a security policy, so do the regular setup. # Here we run tclPkgUnknown in the master, but we hijack # the source command so the setup ends up happening in the slave. rename source source.orig proc source {args} "upvar dir dir interp eval [list $slave] tclPkgSource \[list \$dir\] \$args" if [catch {tclPkgUnknown $package $version $exact} err] { global errorInfo rename source {} rename source.orig source error "$err\n$errorInfo" } rename source {} rename source.orig source # If we are in the first round, check if the package # is now known in the slave: if {$round == 0} { set ifneeded \ [interp eval $slave [list package ifneeded $package $version]] if {"$ifneeded" == ""} { return [tclSafeLoadPkgInternal $slave $package $version $exact 1] } } } proc tclSafeResearchPolicyPath {} { global tclSafePolicyPath auto_index auto_path # If there was no change, do not search again. if {![info exists tclSafePolicyPath]} { set tclSafePolicyPath "" } set oldPolicyPath $tclSafePolicyPath set newPolicyPath [tclSafeComputePolicyPath] if {"$newPolicyPath" == "$oldPolicyPath"} { return } # Loop through the path from back to front so early directories # end up overriding later directories. This code is like auto_load, # but only new-style tclIndex files (version 2) are supported. for {set i [expr [llength $newPolicyPath] - 1]} \ {$i >= 0} \ {incr i -1} { set dir [lindex $newPolicyPath $i] set file [file join $dir tclIndex] if {[file exists $file]} { if {[catch {source $file} msg]} { puts stderr "error sourcing $file: $msg" } } foreach file [lsort [glob -nocomplain [file join $dir *]]] { if {[file isdir $file]} { set dir $file set file [file join $file tclIndex] if {[file exists $file]} { if {[catch {source $file} msg]} { puts stderr "error sourcing $file: $msg" } } } } } } proc tclSafeLoadPolicy {slave package version} { upvar #0 tclSafe$slave state global auto_index set proc ${package}_PolicyInit if {[info command $proc] == "$proc" || [info exists auto_index($proc)]} { if [info exists state(policyLoaded)] { error "security policy $state(policyLoaded) already loaded" } $proc $slave $version interp eval $slave [list package provide $package $version] set state(policyLoaded) $package return 1 } else { return 0 } } # This procedure enables access from a safe interpreter to only a subset of # the subcommands of a command: proc tclSafeSubset {command okpat args} { set subcommand [lindex $args 0] if {[regexp $okpat $subcommand]} { return [eval {$command $subcommand} [lrange $args 1 end]] } error "not allowed to invoke subcommand $subcommand of $command" } # This procedure installs an alias in a slave that invokes "safesubset" # in the master to execute allowed subcommands. It precomputes the pattern # of allowed subcommands; you can use wildcards in the pattern if you wish # to allow subcommand abbreviation. # # Syntax is: tclAliasSubset slave alias target subcommand1 subcommand2... proc tclAliasSubset {slave alias target args} { set pat ^(; set sep "" foreach sub $args { append pat $sep$sub set sep | } append pat )\$ interp alias $slave $alias {} tclSafeSubset $target $pat }