You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1133 lines
33 KiB

# safe.tcl --
#
# This file provide a safe loading/sourcing mechanism for safe interpreters.
# It implements a virtual path mecanism to hide the real pathnames from the
# slave. It runs in a master interpreter and sets up data structure and
# aliases that will be invoked when used from a slave interpreter.
#
# 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.
#
# The implementation is based on namespaces. These naming conventions are
# followed:
# Private procs starts with uppercase.
# Public procs are exported and starts with lowercase
#
# Needed utilities package
package require opt 0.4.1
# Create the safe namespace
namespace eval ::safe {
# Exported API:
namespace export interpCreate interpInit interpConfigure interpDelete \
interpAddToAccessPath interpFindInAccessPath setLogCmd
}
# Helper function to resolve the dual way of specifying staticsok (either
# by -noStatics or -statics 0)
proc ::safe::InterpStatics {} {
foreach v {Args statics noStatics} {
upvar $v $v
}
set flag [::tcl::OptProcArgGiven -noStatics]
if {$flag && (!$noStatics == !$statics)
&& ([::tcl::OptProcArgGiven -statics])} {
return -code error\
"conflicting values given for -statics and -noStatics"
}
if {$flag} {
return [expr {!$noStatics}]
} else {
return $statics
}
}
# Helper function to resolve the dual way of specifying nested loading
# (either by -nestedLoadOk or -nested 1)
proc ::safe::InterpNested {} {
foreach v {Args nested nestedLoadOk} {
upvar $v $v
}
set flag [::tcl::OptProcArgGiven -nestedLoadOk]
# note that the test here is the opposite of the "InterpStatics" one
# (it is not -noNested... because of the wanted default value)
if {$flag && (!$nestedLoadOk != !$nested)
&& ([::tcl::OptProcArgGiven -nested])} {
return -code error\
"conflicting values given for -nested and -nestedLoadOk"
}
if {$flag} {
# another difference with "InterpStatics"
return $nestedLoadOk
} else {
return $nested
}
}
####
#
# API entry points that needs argument parsing :
#
####
# Interface/entry point function and front end for "Create"
proc ::safe::interpCreate {args} {
set Args [::tcl::OptKeyParse ::safe::interpCreate $args]
InterpCreate $slave $accessPath \
[InterpStatics] [InterpNested] $deleteHook
}
proc ::safe::interpInit {args} {
set Args [::tcl::OptKeyParse ::safe::interpIC $args]
if {![::interp exists $slave]} {
return -code error "\"$slave\" is not an interpreter"
}
InterpInit $slave $accessPath \
[InterpStatics] [InterpNested] $deleteHook
}
# Check that the given slave is "one of us"
proc ::safe::CheckInterp {slave} {
namespace upvar ::safe S$slave state
if {![info exists state] || ![::interp exists $slave]} {
return -code error \
"\"$slave\" is not an interpreter managed by ::safe::"
}
}
# Interface/entry point function and front end for "Configure". This code
# is awfully pedestrian because it would need more coupling and support
# between the way we store the configuration values in safe::interp's and
# the Opt package. Obviously we would like an OptConfigure to avoid
# duplicating all this code everywhere.
# -> TODO (the app should share or access easily the program/value stored
# by opt)
# This is even more complicated by the boolean flags with no values that
# we had the bad idea to support for the sake of user simplicity in
# create/init but which makes life hard in configure...
# So this will be hopefully written and some integrated with opt1.0
# (hopefully for tcl8.1 ?)
proc ::safe::interpConfigure {args} {
switch [llength $args] {
1 {
# If we have exactly 1 argument the semantic is to return all
# the current configuration. We still call OptKeyParse though
# we know that "slave" is our given argument because it also
# checks for the "-help" option.
set Args [::tcl::OptKeyParse ::safe::interpIC $args]
CheckInterp $slave
namespace upvar ::safe S$slave state
return [join [list \
[list -accessPath $state(access_path)] \
[list -statics $state(staticsok)] \
[list -nested $state(nestedok)] \
[list -deleteHook $state(cleanupHook)]]]
}
2 {
# If we have exactly 2 arguments the semantic is a "configure
# get"
lassign $args slave arg
# get the flag sub program (we 'know' about Opt's internal
# representation of data)
set desc [lindex [::tcl::OptKeyGetDesc ::safe::interpIC] 2]
set hits [::tcl::OptHits desc $arg]
if {$hits > 1} {
return -code error [::tcl::OptAmbigous $desc $arg]
} elseif {$hits == 0} {
return -code error [::tcl::OptFlagUsage $desc $arg]
}
CheckInterp $slave
namespace upvar ::safe S$slave state
set item [::tcl::OptCurDesc $desc]
set name [::tcl::OptName $item]
switch -exact -- $name {
-accessPath {
return [list -accessPath $state(access_path)]
}
-statics {
return [list -statics $state(staticsok)]
}
-nested {
return [list -nested $state(nestedok)]
}
-deleteHook {
return [list -deleteHook $state(cleanupHook)]
}
-noStatics {
# it is most probably a set in fact but we would need
# then to jump to the set part and it is not *sure*
# that it is a set action that the user want, so force
# it to use the unambigous -statics ?value? instead:
return -code error\
"ambigous query (get or set -noStatics ?)\
use -statics instead"
}
-nestedLoadOk {
return -code error\
"ambigous query (get or set -nestedLoadOk ?)\
use -nested instead"
}
default {
return -code error "unknown flag $name (bug)"
}
}
}
default {
# Otherwise we want to parse the arguments like init and
# create did
set Args [::tcl::OptKeyParse ::safe::interpIC $args]
CheckInterp $slave
namespace upvar ::safe S$slave state
# Get the current (and not the default) values of whatever has
# not been given:
if {![::tcl::OptProcArgGiven -accessPath]} {
set doreset 1
set accessPath $state(access_path)
} else {
set doreset 0
}
if {
![::tcl::OptProcArgGiven -statics]
&& ![::tcl::OptProcArgGiven -noStatics]
} then {
set statics $state(staticsok)
} else {
set statics [InterpStatics]
}
if {
[::tcl::OptProcArgGiven -nested] ||
[::tcl::OptProcArgGiven -nestedLoadOk]
} then {
set nested [InterpNested]
} else {
set nested $state(nestedok)
}
if {![::tcl::OptProcArgGiven -deleteHook]} {
set deleteHook $state(cleanupHook)
}
# we can now reconfigure :
InterpSetConfig $slave $accessPath $statics $nested $deleteHook
# auto_reset the slave (to completly synch the new access_path)
if {$doreset} {
if {[catch {::interp eval $slave {auto_reset}} msg]} {
Log $slave "auto_reset failed: $msg"
} else {
Log $slave "successful auto_reset" NOTICE
}
}
}
}
}
####
#
# Functions that actually implements the exported APIs
#
####
#
# safe::InterpCreate : doing the real job
#
# This procedure creates a safe slave and initializes it with the safe
# base aliases.
# NB: slave name must be simple alphanumeric string, no spaces, no (), no
# {},... {because the state array is stored as part of the name}
#
# Returns the slave name.
#
# Optional Arguments :
# + slave name : if empty, generated name will be used
# + access_path: path list controlling where load/source can occur,
# if empty: the master auto_path will be used.
# + staticsok : flag, if 0 :no static package can be loaded (load {} Xxx)
# if 1 :static packages are ok.
# + nestedok: flag, if 0 :no loading to sub-sub interps (load xx xx sub)
# if 1 : multiple levels are ok.
# use the full name and no indent so auto_mkIndex can find us
proc ::safe::InterpCreate {
slave
access_path
staticsok
nestedok
deletehook
} {
# Create the slave.
if {$slave ne ""} {
::interp create -safe $slave
} else {
# empty argument: generate slave name
set slave [::interp create -safe]
}
Log $slave "Created" NOTICE
# Initialize it. (returns slave name)
InterpInit $slave $access_path $staticsok $nestedok $deletehook
}
#
# InterpSetConfig (was setAccessPath) :
# Sets up slave virtual auto_path and corresponding structure within
# the master. Also sets the tcl_library in the slave to be the first
# directory in the path.
# NB: If you change the path after the slave has been initialized you
# probably need to call "auto_reset" in the slave in order that it gets
# the right auto_index() array values.
proc ::safe::InterpSetConfig {slave access_path staticsok nestedok deletehook} {
global auto_path
# determine and store the access path if empty
if {$access_path eq ""} {
set access_path $auto_path
# Make sure that tcl_library is in auto_path and at the first
# position (needed by setAccessPath)
set where [lsearch -exact $access_path [info library]]
if {$where == -1} {
# not found, add it.
set access_path [linsert $access_path 0 [info library]]
Log $slave "tcl_library was not in auto_path,\
added it to slave's access_path" NOTICE
} elseif {$where != 0} {
# not first, move it first
set access_path [linsert \
[lreplace $access_path $where $where] \
0 [info library]]
Log $slave "tcl_libray was not in first in auto_path,\
moved it to front of slave's access_path" NOTICE
}
# Add 1st level sub dirs (will searched by auto loading from tcl
# code in the slave using glob and thus fail, so we add them here
# so by default it works the same).
set access_path [AddSubDirs $access_path]
}
Log $slave "Setting accessPath=($access_path) staticsok=$staticsok\
nestedok=$nestedok deletehook=($deletehook)" NOTICE
namespace upvar ::safe S$slave state
# clear old autopath if it existed
# build new one
# Extend the access list with the paths used to look for Tcl Modules.
# We save the virtual form separately as well, as syncing it with the
# slave has to be defered until the necessary commands are present for
# setup.
set norm_access_path {}
set slave_access_path {}
set map_access_path {}
set remap_access_path {}
set slave_tm_path {}
set i 0
foreach dir $access_path {
set token [PathToken $i]
lappend slave_access_path $token
lappend map_access_path $token $dir
lappend remap_access_path $dir $token
lappend norm_access_path [file normalize $dir]
incr i
}
set morepaths [::tcl::tm::list]
while {[llength $morepaths]} {
set addpaths $morepaths
set morepaths {}
foreach dir $addpaths {
# Prevent the addition of dirs on the tm list to the
# result if they are already known.
if {[dict exists $remap_access_path $dir]} {
continue
}
set token [PathToken $i]
lappend access_path $dir
lappend slave_access_path $token
lappend map_access_path $token $dir
lappend remap_access_path $dir $token
lappend norm_access_path [file normalize $dir]
lappend slave_tm_path $token
incr i
# [Bug 2854929]
# Recursively find deeper paths which may contain
# modules. Required to handle modules with names like
# 'platform::shell', which translate into
# 'platform/shell-X.tm', i.e arbitrarily deep
# subdirectories.
lappend morepaths {*}[glob -nocomplain -directory $dir -type d *]
}
}
set state(access_path) $access_path
set state(access_path,map) $map_access_path
set state(access_path,remap) $remap_access_path
set state(access_path,norm) $norm_access_path
set state(access_path,slave) $slave_access_path
set state(tm_path_slave) $slave_tm_path
set state(staticsok) $staticsok
set state(nestedok) $nestedok
set state(cleanupHook) $deletehook
SyncAccessPath $slave
}
#
#
# FindInAccessPath:
# Search for a real directory and returns its virtual Id (including the
# "$")
proc ::safe::interpFindInAccessPath {slave path} {
namespace upvar ::safe S$slave state
if {![dict exists $state(access_path,remap) $path]} {
return -code error "$path not found in access path $access_path"
}
return [dict get $state(access_path,remap) $path]
}
#
# addToAccessPath:
# add (if needed) a real directory to access path and return its
# virtual token (including the "$").
proc ::safe::interpAddToAccessPath {slave path} {
# first check if the directory is already in there
# (inlined interpFindInAccessPath).
namespace upvar ::safe S$slave state
if {[dict exists $state(access_path,remap) $path]} {
return [dict get $state(access_path,remap) $path]
}
# new one, add it:
set token [PathToken [llength $state(access_path)]]
lappend state(access_path) $path
lappend state(access_path,slave) $token
lappend state(access_path,map) $token $path
lappend state(access_path,remap) $path $token
lappend state(access_path,norm) [file normalize $path]
SyncAccessPath $slave
return $token
}
# This procedure applies the initializations to an already existing
# interpreter. It is useful when you want to install the safe base aliases
# into a preexisting safe interpreter.
proc ::safe::InterpInit {
slave
access_path
staticsok
nestedok
deletehook
} {
# Configure will generate an access_path when access_path is empty.
InterpSetConfig $slave $access_path $staticsok $nestedok $deletehook
# NB we need to add [namespace current], aliases are always absolute
# paths.
# These aliases let the slave load files to define new commands
# This alias lets the slave use the encoding names, convertfrom,
# convertto, and system, but not "encoding system <name>" to set the
# system encoding.
# Handling Tcl Modules, we need a restricted form of Glob.
# This alias interposes on the 'exit' command and cleanly terminates
# the slave.
foreach {command alias} {
source AliasSource
load AliasLoad
encoding AliasEncoding
exit interpDelete
glob AliasGlob
} {
::interp alias $slave $command {} [namespace current]::$alias $slave
}
# This alias lets the slave have access to a subset of the 'file'
# command functionality.
::interp expose $slave file
foreach subcommand {dirname extension rootname tail} {
::interp alias $slave ::tcl::file::$subcommand {} \
::safe::AliasFileSubcommand $slave $subcommand
}
foreach subcommand {
atime attributes copy delete executable exists isdirectory isfile
link lstat mtime mkdir nativename normalize owned readable readlink
rename size stat tempfile type volumes writable
} {
::interp alias $slave ::tcl::file::$subcommand {} \
::safe::BadSubcommand $slave file $subcommand
}
# Subcommands of info
foreach {subcommand alias} {
nameofexecutable AliasExeName
} {
::interp alias $slave ::tcl::info::$subcommand \
{} [namespace current]::$alias $slave
}
# The allowed slave variables already have been set by Tcl_MakeSafe(3)
# Source init.tcl and tm.tcl into the slave, to get auto_load and
# other procedures defined:
if {[catch {::interp eval $slave {
source [file join $tcl_library init.tcl]
}} msg opt]} {
Log $slave "can't source init.tcl ($msg)"
return -options $opt "can't source init.tcl into slave $slave ($msg)"
}
if {[catch {::interp eval $slave {
source [file join $tcl_library tm.tcl]
}} msg opt]} {
Log $slave "can't source tm.tcl ($msg)"
return -options $opt "can't source tm.tcl into slave $slave ($msg)"
}
# Sync the paths used to search for Tcl modules. This can be done only
# now, after tm.tcl was loaded.
namespace upvar ::safe S$slave state
if {[llength $state(tm_path_slave)] > 0} {
::interp eval $slave [list \
::tcl::tm::add {*}[lreverse $state(tm_path_slave)]]
}
return $slave
}
# Add (only if needed, avoid duplicates) 1 level of sub directories to an
# existing path list. Also removes non directories from the returned
# list.
proc ::safe::AddSubDirs {pathList} {
set res {}
foreach dir $pathList {
if {[file isdirectory $dir]} {
# check that we don't have it yet as a children of a previous
# dir
if {$dir ni $res} {
lappend res $dir
}
foreach sub [glob -directory $dir -nocomplain *] {
if {[file isdirectory $sub] && ($sub ni $res)} {
# new sub dir, add it !
lappend res $sub
}
}
}
}
return $res
}
# This procedure deletes a safe slave managed by Safe Tcl and cleans up
# associated state:
proc ::safe::interpDelete {slave} {
Log $slave "About to delete" NOTICE
namespace upvar ::safe S$slave state
# If the slave has a cleanup hook registered, call it. Check the
# existance because we might be called to delete an interp which has
# not been registered with us at all
if {[info exists state(cleanupHook)]} {
set hook $state(cleanupHook)
if {[llength $hook]} {
# remove the hook now, otherwise if the hook calls us somehow,
# we'll loop
unset state(cleanupHook)
try {
{*}$hook $slave
} on error err {
Log $slave "Delete hook error ($err)"
}
}
}
# Discard the global array of state associated with the slave, and
# delete the interpreter.
if {[info exists state]} {
unset state
}
# if we have been called twice, the interp might have been deleted
# already
if {[::interp exists $slave]} {
::interp delete $slave
Log $slave "Deleted" NOTICE
}
return
}
# Set (or get) the logging mecanism
proc ::safe::setLogCmd {args} {
variable Log
set la [llength $args]
if {$la == 0} {
return $Log
} elseif {$la == 1} {
set Log [lindex $args 0]
} else {
set Log $args
}
if {$Log eq ""} {
# Disable logging completely. Calls to it will be compiled out
# of all users.
proc ::safe::Log {args} {}
} else {
# Activate logging, define proper command.
proc ::safe::Log {slave msg {type ERROR}} {
variable Log
{*}$Log "$type for slave $slave : $msg"
return
}
}
}
# ------------------- END OF PUBLIC METHODS ------------
#
# Sets the slave auto_path to the master recorded value. Also sets
# tcl_library to the first token of the virtual path.
#
proc ::safe::SyncAccessPath {slave} {
namespace upvar ::safe S$slave state
set slave_access_path $state(access_path,slave)
::interp eval $slave [list set auto_path $slave_access_path]
Log $slave "auto_path in $slave has been set to $slave_access_path"\
NOTICE
# This code assumes that info library is the first element in the
# list of auto_path's. See -> InterpSetConfig for the code which
# ensures this condition.
::interp eval $slave [list \
set tcl_library [lindex $slave_access_path 0]]
}
# Returns the virtual token for directory number N.
proc ::safe::PathToken {n} {
# We need to have a ":" in the token string so [file join] on the
# mac won't turn it into a relative path.
return "\$p(:$n:)" ;# Form tested by case 7.2
}
#
# translate virtual path into real path
#
proc ::safe::TranslatePath {slave path} {
namespace upvar ::safe S$slave state
# somehow strip the namespaces 'functionality' out (the danger is that
# we would strip valid macintosh "../" queries... :
if {[string match "*::*" $path] || [string match "*..*" $path]} {
return -code error "invalid characters in path $path"
}
# Use a cached map instead of computed local vars and subst.
return [string map $state(access_path,map) $path]
}
# file name control (limit access to files/resources that should be a
# valid tcl source file)
proc ::safe::CheckFileName {slave file} {
# This used to limit what can be sourced to ".tcl" and forbid files
# with more than 1 dot and longer than 14 chars, but I changed that
# for 8.4 as a safe interp has enough internal protection already to
# allow sourcing anything. - hobbs
if {![file exists $file]} {
# don't tell the file path
return -code error "no such file or directory"
}
if {![file readable $file]} {
# don't tell the file path
return -code error "not readable"
}
}
# AliasFileSubcommand handles selected subcommands of [file] in safe
# interpreters that are *almost* safe. In particular, it just acts to
# prevent discovery of what home directories exist.
proc ::safe::AliasFileSubcommand {slave subcommand name} {
if {[string match ~* $name]} {
set name ./$name
}
tailcall ::interp invokehidden $slave tcl:file:$subcommand $name
}
# AliasGlob is the target of the "glob" alias in safe interpreters.
proc ::safe::AliasGlob {slave args} {
Log $slave "GLOB ! $args" NOTICE
set cmd {}
set at 0
array set got {
-directory 0
-nocomplain 0
-join 0
-tails 0
-- 0
}
if {$::tcl_platform(platform) eq "windows"} {
set dirPartRE {^(.*)[\\/]([^\\/]*)$}
} else {
set dirPartRE {^(.*)/([^/]*)$}
}
set dir {}
set virtualdir {}
while {$at < [llength $args]} {
switch -glob -- [set opt [lindex $args $at]] {
-nocomplain - -- - -join - -tails {
lappend cmd $opt
set got($opt) 1
incr at
}
-types - -type {
lappend cmd -types [lindex $args [incr at]]
incr at
}
-directory {
if {$got($opt)} {
return -code error \
{"-directory" cannot be used with "-path"}
}
set got($opt) 1
set virtualdir [lindex $args [incr at]]
incr at
}
pkgIndex.tcl {
# Oops, this is globbing a subdirectory in regular package
# search. That is not wanted. Abort, handler does catch
# already (because glob was not defined before). See
# package.tcl, lines 484ff in tclPkgUnknown.
return -code error "unknown command glob"
}
-* {
Log $slave "Safe base rejecting glob option '$opt'"
return -code error "Safe base rejecting glob option '$opt'"
}
default {
break
}
}
if {$got(--)} break
}
# Get the real path from the virtual one and check that the path is in the
# access path of that slave. Done after basic argument processing so that
# we know if -nocomplain is set.
if {$got(-directory)} {
try {
set dir [TranslatePath $slave $virtualdir]
DirInAccessPath $slave $dir
} on error msg {
Log $slave $msg
if {$got(-nocomplain)} return
return -code error "permission denied"
}
lappend cmd -directory $dir
}
# Apply the -join semantics ourselves
if {$got(-join)} {
set args [lreplace $args $at end [join [lrange $args $at end] "/"]]
}
# Process remaining pattern arguments
set firstPattern [llength $cmd]
foreach opt [lrange $args $at end] {
if {![regexp $dirPartRE $opt -> thedir thefile]} {
set thedir .
} elseif {[string match ~* $thedir]} {
set thedir ./$thedir
}
if {$thedir eq "*" &&
($thefile eq "pkgIndex.tcl" || $thefile eq "*.tm")} {
set mapped 0
foreach d [glob -directory [TranslatePath $slave $virtualdir] \
-types d -tails *] {
catch {
DirInAccessPath $slave \
[TranslatePath $slave [file join $virtualdir $d]]
lappend cmd [file join $d $thefile]
set mapped 1
}
}
if {$mapped} continue
}
try {
DirInAccessPath $slave [TranslatePath $slave \
[file join $virtualdir $thedir]]
} on error msg {
Log $slave $msg
if {$got(-nocomplain)} continue
return -code error "permission denied"
}
lappend cmd $opt
}
Log $slave "GLOB = $cmd" NOTICE
if {$got(-nocomplain) && [llength $cmd] eq $firstPattern} {
return
}
try {
set entries [::interp invokehidden $slave glob {*}$cmd]
} on error msg {
Log $slave $msg
return -code error "script error"
}
Log $slave "GLOB < $entries" NOTICE
# Translate path back to what the slave should see.
set res {}
set l [string length $dir]
foreach p $entries {
if {[string equal -length $l $dir $p]} {
set p [string replace $p 0 [expr {$l-1}] $virtualdir]
}
lappend res $p
}
Log $slave "GLOB > $res" NOTICE
return $res
}
# AliasSource is the target of the "source" alias in safe interpreters.
proc ::safe::AliasSource {slave args} {
set argc [llength $args]
# Extended for handling of Tcl Modules to allow not only "source
# filename", but "source -encoding E filename" as well.
if {[lindex $args 0] eq "-encoding"} {
incr argc -2
set encoding [lindex $args 1]
set at 2
if {$encoding eq "identity"} {
Log $slave "attempt to use the identity encoding"
return -code error "permission denied"
}
} else {
set at 0
set encoding {}
}
if {$argc != 1} {
set msg "wrong # args: should be \"source ?-encoding E? fileName\""
Log $slave "$msg ($args)"
return -code error $msg
}
set file [lindex $args $at]
# get the real path from the virtual one.
if {[catch {
set realfile [TranslatePath $slave $file]
} msg]} {
Log $slave $msg
return -code error "permission denied"
}
# check that the path is in the access path of that slave
if {[catch {
FileInAccessPath $slave $realfile
} msg]} {
Log $slave $msg
return -code error "permission denied"
}
# do the checks on the filename :
if {[catch {
CheckFileName $slave $realfile
} msg]} {
Log $slave "$realfile:$msg"
return -code error $msg
}
# Passed all the tests, lets source it. Note that we do this all manually
# because we want to control [info script] in the slave so information
# doesn't leak so much. [Bug 2913625]
set old [::interp eval $slave {info script}]
set replacementMsg "script error"
set code [catch {
set f [open $realfile]
fconfigure $f -eofchar \032
if {$encoding ne ""} {
fconfigure $f -encoding $encoding
}
set contents [read $f]
close $f
::interp eval $slave [list info script $file]
} msg opt]
if {$code == 0} {
set code [catch {::interp eval $slave $contents} msg opt]
set replacementMsg $msg
}
catch {interp eval $slave [list info script $old]}
# Note that all non-errors are fine result codes from [source], so we must
# take a little care to do it properly. [Bug 2923613]
if {$code == 1} {
Log $slave $msg
return -code error $replacementMsg
}
return -code $code -options $opt $msg
}
# AliasLoad is the target of the "load" alias in safe interpreters.
proc ::safe::AliasLoad {slave file args} {
set argc [llength $args]
if {$argc > 2} {
set msg "load error: too many arguments"
Log $slave "$msg ($argc) {$file $args}"
return -code error $msg
}
# package name (can be empty if file is not).
set package [lindex $args 0]
namespace upvar ::safe S$slave state
# Determine where to load. load use a relative interp path and {}
# means self, so we can directly and safely use passed arg.
set target [lindex $args 1]
if {$target ne ""} {
# we will try to load into a sub sub interp; check that we want to
# authorize that.
if {!$state(nestedok)} {
Log $slave "loading to a sub interp (nestedok)\
disabled (trying to load $package to $target)"
return -code error "permission denied (nested load)"
}
}
# Determine what kind of load is requested
if {$file eq ""} {
# static package loading
if {$package eq ""} {
set msg "load error: empty filename and no package name"
Log $slave $msg
return -code error $msg
}
if {!$state(staticsok)} {
Log $slave "static packages loading disabled\
(trying to load $package to $target)"
return -code error "permission denied (static package)"
}
} else {
# file loading
# get the real path from the virtual one.
try {
set file [TranslatePath $slave $file]
} on error msg {
Log $slave $msg
return -code error "permission denied"
}
# check the translated path
try {
FileInAccessPath $slave $file
} on error msg {
Log $slave $msg
return -code error "permission denied (path)"
}
}
try {
return [::interp invokehidden $slave load $file $package $target]
} on error msg {
Log $slave $msg
return -code error $msg
}
}
# FileInAccessPath raises an error if the file is not found in the list of
# directories contained in the (master side recorded) slave's access path.
# the security here relies on "file dirname" answering the proper
# result... needs checking ?
proc ::safe::FileInAccessPath {slave file} {
namespace upvar ::safe S$slave state
set access_path $state(access_path)
if {[file isdirectory $file]} {
return -code error "\"$file\": is a directory"
}
set parent [file dirname $file]
# Normalize paths for comparison since lsearch knows nothing of
# potential pathname anomalies.
set norm_parent [file normalize $parent]
namespace upvar ::safe S$slave state
if {$norm_parent ni $state(access_path,norm)} {
return -code error "\"$file\": not in access_path"
}
}
proc ::safe::DirInAccessPath {slave dir} {
namespace upvar ::safe S$slave state
set access_path $state(access_path)
if {[file isfile $dir]} {
return -code error "\"$dir\": is a file"
}
# Normalize paths for comparison since lsearch knows nothing of
# potential pathname anomalies.
set norm_dir [file normalize $dir]
namespace upvar ::safe S$slave state
if {$norm_dir ni $state(access_path,norm)} {
return -code error "\"$dir\": not in access_path"
}
}
# This procedure is used to report an attempt to use an unsafe member of an
# ensemble command.
proc ::safe::BadSubcommand {slave command subcommand args} {
set msg "not allowed to invoke subcommand $subcommand of $command"
Log $slave $msg
return -code error -errorcode {TCL SAFE SUBCOMMAND} $msg
}
# AliasEncoding is the target of the "encoding" alias in safe interpreters.
proc ::safe::AliasEncoding {slave option args} {
# Note that [encoding dirs] is not supported in safe slaves at all
set subcommands {convertfrom convertto names system}
try {
set option [tcl::prefix match -error [list -level 1 -errorcode \
[list TCL LOOKUP INDEX option $option]] $subcommands $option]
# Special case: [encoding system] ok, but [encoding system foo] not
if {$option eq "system" && [llength $args]} {
return -code error -errorcode {TCL WRONGARGS} \
"wrong # args: should be \"encoding system\""
}
} on error {msg options} {
Log $slave $msg
return -options $options $msg
}
tailcall ::interp invokehidden $slave encoding $option {*}$args
}
# Various minor hiding of platform features. [Bug 2913625]
proc ::safe::AliasExeName {slave} {
return ""
}
proc ::safe::Setup {} {
####
#
# Setup the arguments parsing
#
####
# Share the descriptions
set temp [::tcl::OptKeyRegister {
{-accessPath -list {} "access path for the slave"}
{-noStatics "prevent loading of statically linked pkgs"}
{-statics true "loading of statically linked pkgs"}
{-nestedLoadOk "allow nested loading"}
{-nested false "nested loading"}
{-deleteHook -script {} "delete hook"}
}]
# create case (slave is optional)
::tcl::OptKeyRegister {
{?slave? -name {} "name of the slave (optional)"}
} ::safe::interpCreate
# adding the flags sub programs to the command program (relying on Opt's
# internal implementation details)
lappend ::tcl::OptDesc(::safe::interpCreate) $::tcl::OptDesc($temp)
# init and configure (slave is needed)
::tcl::OptKeyRegister {
{slave -name {} "name of the slave"}
} ::safe::interpIC
# adding the flags sub programs to the command program (relying on Opt's
# internal implementation details)
lappend ::tcl::OptDesc(::safe::interpIC) $::tcl::OptDesc($temp)
# temp not needed anymore
::tcl::OptKeyDelete $temp
####
#
# Default: No logging.
#
####
setLogCmd {}
# Log eventually.
# To enable error logging, set Log to {puts stderr} for instance,
# via setLogCmd.
return
}
namespace eval ::safe {
# internal variables
# Log command, set via 'setLogCmd'. Logging is disabled when empty.
variable Log {}
# The package maintains a state array per slave interp under its
# control. The name of this array is S<interp-name>. This array is
# brought into scope where needed, using 'namespace upvar'. The S
# prefix is used to avoid that a slave interp called "Log" smashes
# the "Log" variable.
#
# The array's elements are:
#
# access_path : List of paths accessible to the slave.
# access_path,norm : Ditto, in normalized form.
# access_path,slave : Ditto, as the path tokens as seen by the slave.
# access_path,map : dict ( token -> path )
# access_path,remap : dict ( path -> token )
# tm_path_slave : List of TM root directories, as tokens seen by the slave.
# staticsok : Value of option -statics
# nestedok : Value of option -nested
# cleanupHook : Value of option -deleteHook
}
::safe::Setup