figenc

[RADIOACTIVE] rsa and symmetric key encryption scripts and executables
git clone git://git.figbert.com/figenc.git
Log | Files | Refs | README

safe.tcl (33439B)


      1 # safe.tcl --
      2 #
      3 # This file provide a safe loading/sourcing mechanism for safe interpreters.
      4 # It implements a virtual path mecanism to hide the real pathnames from the
      5 # slave. It runs in a master interpreter and sets up data structure and
      6 # aliases that will be invoked when used from a slave interpreter.
      7 #
      8 # See the safe.n man page for details.
      9 #
     10 # Copyright (c) 1996-1997 Sun Microsystems, Inc.
     11 #
     12 # See the file "license.terms" for information on usage and redistribution of
     13 # this file, and for a DISCLAIMER OF ALL WARRANTIES.
     14 
     15 #
     16 # The implementation is based on namespaces. These naming conventions are
     17 # followed:
     18 # Private procs starts with uppercase.
     19 # Public  procs are exported and starts with lowercase
     20 #
     21 
     22 # Needed utilities package
     23 package require opt 0.4.1
     24 
     25 # Create the safe namespace
     26 namespace eval ::safe {
     27     # Exported API:
     28     namespace export interpCreate interpInit interpConfigure interpDelete \
     29 	interpAddToAccessPath interpFindInAccessPath setLogCmd
     30 }
     31 
     32 # Helper function to resolve the dual way of specifying staticsok (either
     33 # by -noStatics or -statics 0)
     34 proc ::safe::InterpStatics {} {
     35     foreach v {Args statics noStatics} {
     36 	upvar $v $v
     37     }
     38     set flag [::tcl::OptProcArgGiven -noStatics]
     39     if {$flag && (!$noStatics == !$statics)
     40 	&& ([::tcl::OptProcArgGiven -statics])} {
     41 	return -code error\
     42 	    "conflicting values given for -statics and -noStatics"
     43     }
     44     if {$flag} {
     45 	return [expr {!$noStatics}]
     46     } else {
     47 	return $statics
     48     }
     49 }
     50 
     51 # Helper function to resolve the dual way of specifying nested loading
     52 # (either by -nestedLoadOk or -nested 1)
     53 proc ::safe::InterpNested {} {
     54     foreach v {Args nested nestedLoadOk} {
     55 	upvar $v $v
     56     }
     57     set flag [::tcl::OptProcArgGiven -nestedLoadOk]
     58     # note that the test here is the opposite of the "InterpStatics" one
     59     # (it is not -noNested... because of the wanted default value)
     60     if {$flag && (!$nestedLoadOk != !$nested)
     61 	&& ([::tcl::OptProcArgGiven -nested])} {
     62 	return -code error\
     63 	    "conflicting values given for -nested and -nestedLoadOk"
     64     }
     65     if {$flag} {
     66 	# another difference with "InterpStatics"
     67 	return $nestedLoadOk
     68     } else {
     69 	return $nested
     70     }
     71 }
     72 
     73 ####
     74 #
     75 #  API entry points that needs argument parsing :
     76 #
     77 ####
     78 
     79 # Interface/entry point function and front end for "Create"
     80 proc ::safe::interpCreate {args} {
     81     set Args [::tcl::OptKeyParse ::safe::interpCreate $args]
     82     InterpCreate $slave $accessPath \
     83 	[InterpStatics] [InterpNested] $deleteHook
     84 }
     85 
     86 proc ::safe::interpInit {args} {
     87     set Args [::tcl::OptKeyParse ::safe::interpIC $args]
     88     if {![::interp exists $slave]} {
     89 	return -code error "\"$slave\" is not an interpreter"
     90     }
     91     InterpInit $slave $accessPath \
     92 	[InterpStatics] [InterpNested] $deleteHook
     93 }
     94 
     95 # Check that the given slave is "one of us"
     96 proc ::safe::CheckInterp {slave} {
     97     namespace upvar ::safe S$slave state
     98     if {![info exists state] || ![::interp exists $slave]} {
     99 	return -code error \
    100 	    "\"$slave\" is not an interpreter managed by ::safe::"
    101     }
    102 }
    103 
    104 # Interface/entry point function and front end for "Configure".  This code
    105 # is awfully pedestrian because it would need more coupling and support
    106 # between the way we store the configuration values in safe::interp's and
    107 # the Opt package. Obviously we would like an OptConfigure to avoid
    108 # duplicating all this code everywhere.
    109 # -> TODO (the app should share or access easily the program/value stored
    110 # by opt)
    111 
    112 # This is even more complicated by the boolean flags with no values that
    113 # we had the bad idea to support for the sake of user simplicity in
    114 # create/init but which makes life hard in configure...
    115 # So this will be hopefully written and some integrated with opt1.0
    116 # (hopefully for tcl8.1 ?)
    117 proc ::safe::interpConfigure {args} {
    118     switch [llength $args] {
    119 	1 {
    120 	    # If we have exactly 1 argument the semantic is to return all
    121 	    # the current configuration. We still call OptKeyParse though
    122 	    # we know that "slave" is our given argument because it also
    123 	    # checks for the "-help" option.
    124 	    set Args [::tcl::OptKeyParse ::safe::interpIC $args]
    125 	    CheckInterp $slave
    126 	    namespace upvar ::safe S$slave state
    127 
    128 	    return [join [list \
    129 		[list -accessPath $state(access_path)] \
    130 		[list -statics    $state(staticsok)]   \
    131 		[list -nested     $state(nestedok)]    \
    132 	        [list -deleteHook $state(cleanupHook)]]]
    133 	}
    134 	2 {
    135 	    # If we have exactly 2 arguments the semantic is a "configure
    136 	    # get"
    137 	    lassign $args slave arg
    138 
    139 	    # get the flag sub program (we 'know' about Opt's internal
    140 	    # representation of data)
    141 	    set desc [lindex [::tcl::OptKeyGetDesc ::safe::interpIC] 2]
    142 	    set hits [::tcl::OptHits desc $arg]
    143 	    if {$hits > 1} {
    144 		return -code error [::tcl::OptAmbigous $desc $arg]
    145 	    } elseif {$hits == 0} {
    146 		return -code error [::tcl::OptFlagUsage $desc $arg]
    147 	    }
    148 	    CheckInterp $slave
    149 	    namespace upvar ::safe S$slave state
    150 
    151 	    set item [::tcl::OptCurDesc $desc]
    152 	    set name [::tcl::OptName $item]
    153 	    switch -exact -- $name {
    154 		-accessPath {
    155 		    return [list -accessPath $state(access_path)]
    156 		}
    157 		-statics    {
    158 		    return [list -statics $state(staticsok)]
    159 		}
    160 		-nested     {
    161 		    return [list -nested $state(nestedok)]
    162 		}
    163 		-deleteHook {
    164 		    return [list -deleteHook $state(cleanupHook)]
    165 		}
    166 		-noStatics {
    167 		    # it is most probably a set in fact but we would need
    168 		    # then to jump to the set part and it is not *sure*
    169 		    # that it is a set action that the user want, so force
    170 		    # it to use the unambigous -statics ?value? instead:
    171 		    return -code error\
    172 			"ambigous query (get or set -noStatics ?)\
    173 				use -statics instead"
    174 		}
    175 		-nestedLoadOk {
    176 		    return -code error\
    177 			"ambigous query (get or set -nestedLoadOk ?)\
    178 				use -nested instead"
    179 		}
    180 		default {
    181 		    return -code error "unknown flag $name (bug)"
    182 		}
    183 	    }
    184 	}
    185 	default {
    186 	    # Otherwise we want to parse the arguments like init and
    187 	    # create did
    188 	    set Args [::tcl::OptKeyParse ::safe::interpIC $args]
    189 	    CheckInterp $slave
    190 	    namespace upvar ::safe S$slave state
    191 
    192 	    # Get the current (and not the default) values of whatever has
    193 	    # not been given:
    194 	    if {![::tcl::OptProcArgGiven -accessPath]} {
    195 		set doreset 1
    196 		set accessPath $state(access_path)
    197 	    } else {
    198 		set doreset 0
    199 	    }
    200 	    if {
    201 		![::tcl::OptProcArgGiven -statics]
    202 		&& ![::tcl::OptProcArgGiven -noStatics]
    203 	    } then {
    204 		set statics    $state(staticsok)
    205 	    } else {
    206 		set statics    [InterpStatics]
    207 	    }
    208 	    if {
    209 		[::tcl::OptProcArgGiven -nested] ||
    210 		[::tcl::OptProcArgGiven -nestedLoadOk]
    211 	    } then {
    212 		set nested     [InterpNested]
    213 	    } else {
    214 		set nested     $state(nestedok)
    215 	    }
    216 	    if {![::tcl::OptProcArgGiven -deleteHook]} {
    217 		set deleteHook $state(cleanupHook)
    218 	    }
    219 	    # we can now reconfigure :
    220 	    InterpSetConfig $slave $accessPath $statics $nested $deleteHook
    221 	    # auto_reset the slave (to completly synch the new access_path)
    222 	    if {$doreset} {
    223 		if {[catch {::interp eval $slave {auto_reset}} msg]} {
    224 		    Log $slave "auto_reset failed: $msg"
    225 		} else {
    226 		    Log $slave "successful auto_reset" NOTICE
    227 		}
    228 	    }
    229 	}
    230     }
    231 }
    232 
    233 ####
    234 #
    235 #  Functions that actually implements the exported APIs
    236 #
    237 ####
    238 
    239 #
    240 # safe::InterpCreate : doing the real job
    241 #
    242 # This procedure creates a safe slave and initializes it with the safe
    243 # base aliases.
    244 # NB: slave name must be simple alphanumeric string, no spaces, no (), no
    245 # {},...  {because the state array is stored as part of the name}
    246 #
    247 # Returns the slave name.
    248 #
    249 # Optional Arguments :
    250 # + slave name : if empty, generated name will be used
    251 # + access_path: path list controlling where load/source can occur,
    252 #                if empty: the master auto_path will be used.
    253 # + staticsok  : flag, if 0 :no static package can be loaded (load {} Xxx)
    254 #                      if 1 :static packages are ok.
    255 # + nestedok: flag, if 0 :no loading to sub-sub interps (load xx xx sub)
    256 #                      if 1 : multiple levels are ok.
    257 
    258 # use the full name and no indent so auto_mkIndex can find us
    259 proc ::safe::InterpCreate {
    260 			   slave
    261 			   access_path
    262 			   staticsok
    263 			   nestedok
    264 			   deletehook
    265 		       } {
    266     # Create the slave.
    267     if {$slave ne ""} {
    268 	::interp create -safe $slave
    269     } else {
    270 	# empty argument: generate slave name
    271 	set slave [::interp create -safe]
    272     }
    273     Log $slave "Created" NOTICE
    274 
    275     # Initialize it. (returns slave name)
    276     InterpInit $slave $access_path $staticsok $nestedok $deletehook
    277 }
    278 
    279 #
    280 # InterpSetConfig (was setAccessPath) :
    281 #    Sets up slave virtual auto_path and corresponding structure within
    282 #    the master. Also sets the tcl_library in the slave to be the first
    283 #    directory in the path.
    284 #    NB: If you change the path after the slave has been initialized you
    285 #    probably need to call "auto_reset" in the slave in order that it gets
    286 #    the right auto_index() array values.
    287 
    288 proc ::safe::InterpSetConfig {slave access_path staticsok nestedok deletehook} {
    289     global auto_path
    290 
    291     # determine and store the access path if empty
    292     if {$access_path eq ""} {
    293 	set access_path $auto_path
    294 
    295 	# Make sure that tcl_library is in auto_path and at the first
    296 	# position (needed by setAccessPath)
    297 	set where [lsearch -exact $access_path [info library]]
    298 	if {$where == -1} {
    299 	    # not found, add it.
    300 	    set access_path [linsert $access_path 0 [info library]]
    301 	    Log $slave "tcl_library was not in auto_path,\
    302 			added it to slave's access_path" NOTICE
    303 	} elseif {$where != 0} {
    304 	    # not first, move it first
    305 	    set access_path [linsert \
    306 				 [lreplace $access_path $where $where] \
    307 				 0 [info library]]
    308 	    Log $slave "tcl_libray was not in first in auto_path,\
    309 			moved it to front of slave's access_path" NOTICE
    310 	}
    311 
    312 	# Add 1st level sub dirs (will searched by auto loading from tcl
    313 	# code in the slave using glob and thus fail, so we add them here
    314 	# so by default it works the same).
    315 	set access_path [AddSubDirs $access_path]
    316     }
    317 
    318     Log $slave "Setting accessPath=($access_path) staticsok=$staticsok\
    319 		nestedok=$nestedok deletehook=($deletehook)" NOTICE
    320 
    321     namespace upvar ::safe S$slave state
    322 
    323     # clear old autopath if it existed
    324     # build new one
    325     # Extend the access list with the paths used to look for Tcl Modules.
    326     # We save the virtual form separately as well, as syncing it with the
    327     # slave has to be defered until the necessary commands are present for
    328     # setup.
    329 
    330     set norm_access_path  {}
    331     set slave_access_path {}
    332     set map_access_path   {}
    333     set remap_access_path {}
    334     set slave_tm_path     {}
    335 
    336     set i 0
    337     foreach dir $access_path {
    338 	set token [PathToken $i]
    339 	lappend slave_access_path  $token
    340 	lappend map_access_path    $token $dir
    341 	lappend remap_access_path  $dir $token
    342 	lappend norm_access_path   [file normalize $dir]
    343 	incr i
    344     }
    345 
    346     set morepaths [::tcl::tm::list]
    347     while {[llength $morepaths]} {
    348 	set addpaths $morepaths
    349 	set morepaths {}
    350 
    351 	foreach dir $addpaths {
    352 	    # Prevent the addition of dirs on the tm list to the
    353 	    # result if they are already known.
    354 	    if {[dict exists $remap_access_path $dir]} {
    355 		continue
    356 	    }
    357 
    358 	    set token [PathToken $i]
    359 	    lappend access_path        $dir
    360 	    lappend slave_access_path  $token
    361 	    lappend map_access_path    $token $dir
    362 	    lappend remap_access_path  $dir $token
    363 	    lappend norm_access_path   [file normalize $dir]
    364 	    lappend slave_tm_path $token
    365 	    incr i
    366 
    367 	    # [Bug 2854929]
    368 	    # Recursively find deeper paths which may contain
    369 	    # modules. Required to handle modules with names like
    370 	    # 'platform::shell', which translate into
    371 	    # 'platform/shell-X.tm', i.e arbitrarily deep
    372 	    # subdirectories.
    373 	    lappend morepaths {*}[glob -nocomplain -directory $dir -type d *]
    374 	}
    375     }
    376 
    377     set state(access_path)       $access_path
    378     set state(access_path,map)   $map_access_path
    379     set state(access_path,remap) $remap_access_path
    380     set state(access_path,norm)  $norm_access_path
    381     set state(access_path,slave) $slave_access_path
    382     set state(tm_path_slave)     $slave_tm_path
    383     set state(staticsok)         $staticsok
    384     set state(nestedok)          $nestedok
    385     set state(cleanupHook)       $deletehook
    386 
    387     SyncAccessPath $slave
    388 }
    389 
    390 #
    391 #
    392 # FindInAccessPath:
    393 #    Search for a real directory and returns its virtual Id (including the
    394 #    "$")
    395 proc ::safe::interpFindInAccessPath {slave path} {
    396     namespace upvar ::safe S$slave state
    397 
    398     if {![dict exists $state(access_path,remap) $path]} {
    399 	return -code error "$path not found in access path $access_path"
    400     }
    401 
    402     return [dict get $state(access_path,remap) $path]
    403 }
    404 
    405 #
    406 # addToAccessPath:
    407 #    add (if needed) a real directory to access path and return its
    408 #    virtual token (including the "$").
    409 proc ::safe::interpAddToAccessPath {slave path} {
    410     # first check if the directory is already in there
    411     # (inlined interpFindInAccessPath).
    412     namespace upvar ::safe S$slave state
    413 
    414     if {[dict exists $state(access_path,remap) $path]} {
    415 	return [dict get $state(access_path,remap) $path]
    416     }
    417 
    418     # new one, add it:
    419     set token [PathToken [llength $state(access_path)]]
    420 
    421     lappend state(access_path)       $path
    422     lappend state(access_path,slave) $token
    423     lappend state(access_path,map)   $token $path
    424     lappend state(access_path,remap) $path $token
    425     lappend state(access_path,norm)  [file normalize $path]
    426 
    427     SyncAccessPath $slave
    428     return $token
    429 }
    430 
    431 # This procedure applies the initializations to an already existing
    432 # interpreter. It is useful when you want to install the safe base aliases
    433 # into a preexisting safe interpreter.
    434 proc ::safe::InterpInit {
    435 			 slave
    436 			 access_path
    437 			 staticsok
    438 			 nestedok
    439 			 deletehook
    440 		     } {
    441     # Configure will generate an access_path when access_path is empty.
    442     InterpSetConfig $slave $access_path $staticsok $nestedok $deletehook
    443 
    444     # NB we need to add [namespace current], aliases are always absolute
    445     # paths.
    446 
    447     # These aliases let the slave load files to define new commands
    448     # This alias lets the slave use the encoding names, convertfrom,
    449     # convertto, and system, but not "encoding system <name>" to set the
    450     # system encoding.
    451     # Handling Tcl Modules, we need a restricted form of Glob.
    452     # This alias interposes on the 'exit' command and cleanly terminates
    453     # the slave.
    454 
    455     foreach {command alias} {
    456 	source   AliasSource
    457 	load     AliasLoad
    458 	encoding AliasEncoding
    459 	exit     interpDelete
    460 	glob     AliasGlob
    461     } {
    462 	::interp alias $slave $command {} [namespace current]::$alias $slave
    463     }
    464 
    465     # This alias lets the slave have access to a subset of the 'file'
    466     # command functionality.
    467 
    468     ::interp expose $slave file
    469     foreach subcommand {dirname extension rootname tail} {
    470 	::interp alias $slave ::tcl::file::$subcommand {} \
    471 	    ::safe::AliasFileSubcommand $slave $subcommand
    472     }
    473     foreach subcommand {
    474 	atime attributes copy delete executable exists isdirectory isfile
    475 	link lstat mtime mkdir nativename normalize owned readable readlink
    476 	rename size stat tempfile type volumes writable
    477     } {
    478 	::interp alias $slave ::tcl::file::$subcommand {} \
    479 	    ::safe::BadSubcommand $slave file $subcommand
    480     }
    481 
    482     # Subcommands of info
    483     foreach {subcommand alias} {
    484 	nameofexecutable   AliasExeName
    485     } {
    486 	::interp alias $slave ::tcl::info::$subcommand \
    487 	    {} [namespace current]::$alias $slave
    488     }
    489 
    490     # The allowed slave variables already have been set by Tcl_MakeSafe(3)
    491 
    492     # Source init.tcl and tm.tcl into the slave, to get auto_load and
    493     # other procedures defined:
    494 
    495     if {[catch {::interp eval $slave {
    496 	source [file join $tcl_library init.tcl]
    497     }} msg opt]} {
    498 	Log $slave "can't source init.tcl ($msg)"
    499 	return -options $opt "can't source init.tcl into slave $slave ($msg)"
    500     }
    501 
    502     if {[catch {::interp eval $slave {
    503 	source [file join $tcl_library tm.tcl]
    504     }} msg opt]} {
    505 	Log $slave "can't source tm.tcl ($msg)"
    506 	return -options $opt "can't source tm.tcl into slave $slave ($msg)"
    507     }
    508 
    509     # Sync the paths used to search for Tcl modules. This can be done only
    510     # now, after tm.tcl was loaded.
    511     namespace upvar ::safe S$slave state
    512     if {[llength $state(tm_path_slave)] > 0} {
    513 	::interp eval $slave [list \
    514 		::tcl::tm::add {*}[lreverse $state(tm_path_slave)]]
    515     }
    516     return $slave
    517 }
    518 
    519 # Add (only if needed, avoid duplicates) 1 level of sub directories to an
    520 # existing path list.  Also removes non directories from the returned
    521 # list.
    522 proc ::safe::AddSubDirs {pathList} {
    523     set res {}
    524     foreach dir $pathList {
    525 	if {[file isdirectory $dir]} {
    526 	    # check that we don't have it yet as a children of a previous
    527 	    # dir
    528 	    if {$dir ni $res} {
    529 		lappend res $dir
    530 	    }
    531 	    foreach sub [glob -directory $dir -nocomplain *] {
    532 		if {[file isdirectory $sub] && ($sub ni $res)} {
    533 		    # new sub dir, add it !
    534 		    lappend res $sub
    535 		}
    536 	    }
    537 	}
    538     }
    539     return $res
    540 }
    541 
    542 # This procedure deletes a safe slave managed by Safe Tcl and cleans up
    543 # associated state:
    544 
    545 proc ::safe::interpDelete {slave} {
    546     Log $slave "About to delete" NOTICE
    547 
    548     namespace upvar ::safe S$slave state
    549 
    550     # If the slave has a cleanup hook registered, call it.  Check the
    551     # existance because we might be called to delete an interp which has
    552     # not been registered with us at all
    553 
    554     if {[info exists state(cleanupHook)]} {
    555 	set hook $state(cleanupHook)
    556 	if {[llength $hook]} {
    557 	    # remove the hook now, otherwise if the hook calls us somehow,
    558 	    # we'll loop
    559 	    unset state(cleanupHook)
    560 	    try {
    561 		{*}$hook $slave
    562 	    } on error err {
    563 		Log $slave "Delete hook error ($err)"
    564 	    }
    565 	}
    566     }
    567 
    568     # Discard the global array of state associated with the slave, and
    569     # delete the interpreter.
    570 
    571     if {[info exists state]} {
    572 	unset state
    573     }
    574 
    575     # if we have been called twice, the interp might have been deleted
    576     # already
    577     if {[::interp exists $slave]} {
    578 	::interp delete $slave
    579 	Log $slave "Deleted" NOTICE
    580     }
    581 
    582     return
    583 }
    584 
    585 # Set (or get) the logging mecanism
    586 
    587 proc ::safe::setLogCmd {args} {
    588     variable Log
    589     set la [llength $args]
    590     if {$la == 0} {
    591 	return $Log
    592     } elseif {$la == 1} {
    593 	set Log [lindex $args 0]
    594     } else {
    595 	set Log $args
    596     }
    597 
    598     if {$Log eq ""} {
    599 	# Disable logging completely. Calls to it will be compiled out
    600 	# of all users.
    601 	proc ::safe::Log {args} {}
    602     } else {
    603 	# Activate logging, define proper command.
    604 
    605 	proc ::safe::Log {slave msg {type ERROR}} {
    606 	    variable Log
    607 	    {*}$Log "$type for slave $slave : $msg"
    608 	    return
    609 	}
    610     }
    611 }
    612 
    613 # ------------------- END OF PUBLIC METHODS ------------
    614 
    615 #
    616 # Sets the slave auto_path to the master recorded value.  Also sets
    617 # tcl_library to the first token of the virtual path.
    618 #
    619 proc ::safe::SyncAccessPath {slave} {
    620     namespace upvar ::safe S$slave state
    621 
    622     set slave_access_path $state(access_path,slave)
    623     ::interp eval $slave [list set auto_path $slave_access_path]
    624 
    625     Log $slave "auto_path in $slave has been set to $slave_access_path"\
    626 	NOTICE
    627 
    628     # This code assumes that info library is the first element in the
    629     # list of auto_path's. See -> InterpSetConfig for the code which
    630     # ensures this condition.
    631 
    632     ::interp eval $slave [list \
    633 	      set tcl_library [lindex $slave_access_path 0]]
    634 }
    635 
    636 # Returns the virtual token for directory number N.
    637 proc ::safe::PathToken {n} {
    638     # We need to have a ":" in the token string so [file join] on the
    639     # mac won't turn it into a relative path.
    640     return "\$p(:$n:)" ;# Form tested by case 7.2
    641 }
    642 
    643 #
    644 # translate virtual path into real path
    645 #
    646 proc ::safe::TranslatePath {slave path} {
    647     namespace upvar ::safe S$slave state
    648 
    649     # somehow strip the namespaces 'functionality' out (the danger is that
    650     # we would strip valid macintosh "../" queries... :
    651     if {[string match "*::*" $path] || [string match "*..*" $path]} {
    652 	return -code error "invalid characters in path $path"
    653     }
    654 
    655     # Use a cached map instead of computed local vars and subst.
    656 
    657     return [string map $state(access_path,map) $path]
    658 }
    659 
    660 # file name control (limit access to files/resources that should be a
    661 # valid tcl source file)
    662 proc ::safe::CheckFileName {slave file} {
    663     # This used to limit what can be sourced to ".tcl" and forbid files
    664     # with more than 1 dot and longer than 14 chars, but I changed that
    665     # for 8.4 as a safe interp has enough internal protection already to
    666     # allow sourcing anything. - hobbs
    667 
    668     if {![file exists $file]} {
    669 	# don't tell the file path
    670 	return -code error "no such file or directory"
    671     }
    672 
    673     if {![file readable $file]} {
    674 	# don't tell the file path
    675 	return -code error "not readable"
    676     }
    677 }
    678 
    679 # AliasFileSubcommand handles selected subcommands of [file] in safe
    680 # interpreters that are *almost* safe. In particular, it just acts to
    681 # prevent discovery of what home directories exist.
    682 
    683 proc ::safe::AliasFileSubcommand {slave subcommand name} {
    684     if {[string match ~* $name]} {
    685 	set name ./$name
    686     }
    687     tailcall ::interp invokehidden $slave tcl:file:$subcommand $name
    688 }
    689 
    690 # AliasGlob is the target of the "glob" alias in safe interpreters.
    691 
    692 proc ::safe::AliasGlob {slave args} {
    693     Log $slave "GLOB ! $args" NOTICE
    694     set cmd {}
    695     set at 0
    696     array set got {
    697 	-directory 0
    698 	-nocomplain 0
    699 	-join 0
    700 	-tails 0
    701 	-- 0
    702     }
    703 
    704     if {$::tcl_platform(platform) eq "windows"} {
    705 	set dirPartRE {^(.*)[\\/]([^\\/]*)$}
    706     } else {
    707 	set dirPartRE {^(.*)/([^/]*)$}
    708     }
    709 
    710     set dir        {}
    711     set virtualdir {}
    712 
    713     while {$at < [llength $args]} {
    714 	switch -glob -- [set opt [lindex $args $at]] {
    715 	    -nocomplain - -- - -join - -tails {
    716 		lappend cmd $opt
    717 		set got($opt) 1
    718 		incr at
    719 	    }
    720 	    -types - -type {
    721 		lappend cmd -types [lindex $args [incr at]]
    722 		incr at
    723 	    }
    724 	    -directory {
    725 		if {$got($opt)} {
    726 		    return -code error \
    727 			{"-directory" cannot be used with "-path"}
    728 		}
    729 		set got($opt) 1
    730 		set virtualdir [lindex $args [incr at]]
    731 		incr at
    732 	    }
    733 	    pkgIndex.tcl {
    734 		# Oops, this is globbing a subdirectory in regular package
    735 		# search. That is not wanted. Abort, handler does catch
    736 		# already (because glob was not defined before). See
    737 		# package.tcl, lines 484ff in tclPkgUnknown.
    738 		return -code error "unknown command glob"
    739 	    }
    740 	    -* {
    741 		Log $slave "Safe base rejecting glob option '$opt'"
    742 		return -code error "Safe base rejecting glob option '$opt'"
    743 	    }
    744 	    default {
    745 		break
    746 	    }
    747 	}
    748 	if {$got(--)} break
    749     }
    750 
    751     # Get the real path from the virtual one and check that the path is in the
    752     # access path of that slave. Done after basic argument processing so that
    753     # we know if -nocomplain is set.
    754     if {$got(-directory)} {
    755 	try {
    756 	    set dir [TranslatePath $slave $virtualdir]
    757 	    DirInAccessPath $slave $dir
    758 	} on error msg {
    759 	    Log $slave $msg
    760 	    if {$got(-nocomplain)} return
    761 	    return -code error "permission denied"
    762 	}
    763 	lappend cmd -directory $dir
    764     }
    765 
    766     # Apply the -join semantics ourselves
    767     if {$got(-join)} {
    768 	set args [lreplace $args $at end [join [lrange $args $at end] "/"]]
    769     }
    770 
    771     # Process remaining pattern arguments
    772     set firstPattern [llength $cmd]
    773     foreach opt [lrange $args $at end] {
    774 	if {![regexp $dirPartRE $opt -> thedir thefile]} {
    775 	    set thedir .
    776 	} elseif {[string match ~* $thedir]} {
    777 	    set thedir ./$thedir
    778 	}
    779 	if {$thedir eq "*" &&
    780 		($thefile eq "pkgIndex.tcl" || $thefile eq "*.tm")} {
    781 	    set mapped 0
    782 	    foreach d [glob -directory [TranslatePath $slave $virtualdir] \
    783 			   -types d -tails *] {
    784 		catch {
    785 		    DirInAccessPath $slave \
    786 			[TranslatePath $slave [file join $virtualdir $d]]
    787 		    lappend cmd [file join $d $thefile]
    788 		    set mapped 1
    789 		}
    790 	    }
    791 	    if {$mapped} continue
    792 	}
    793 	try {
    794 	    DirInAccessPath $slave [TranslatePath $slave \
    795 		    [file join $virtualdir $thedir]]
    796 	} on error msg {
    797 	    Log $slave $msg
    798 	    if {$got(-nocomplain)} continue
    799 	    return -code error "permission denied"
    800 	}
    801 	lappend cmd $opt
    802     }
    803 
    804     Log $slave "GLOB = $cmd" NOTICE
    805 
    806     if {$got(-nocomplain) && [llength $cmd] eq $firstPattern} {
    807 	return
    808     }
    809     try {
    810 	set entries [::interp invokehidden $slave glob {*}$cmd]
    811     } on error msg {
    812 	Log $slave $msg
    813 	return -code error "script error"
    814     }
    815 
    816     Log $slave "GLOB < $entries" NOTICE
    817 
    818     # Translate path back to what the slave should see.
    819     set res {}
    820     set l [string length $dir]
    821     foreach p $entries {
    822 	if {[string equal -length $l $dir $p]} {
    823 	    set p [string replace $p 0 [expr {$l-1}] $virtualdir]
    824 	}
    825 	lappend res $p
    826     }
    827 
    828     Log $slave "GLOB > $res" NOTICE
    829     return $res
    830 }
    831 
    832 # AliasSource is the target of the "source" alias in safe interpreters.
    833 
    834 proc ::safe::AliasSource {slave args} {
    835     set argc [llength $args]
    836     # Extended for handling of Tcl Modules to allow not only "source
    837     # filename", but "source -encoding E filename" as well.
    838     if {[lindex $args 0] eq "-encoding"} {
    839 	incr argc -2
    840 	set encoding [lindex $args 1]
    841 	set at 2
    842 	if {$encoding eq "identity"} {
    843 	    Log $slave "attempt to use the identity encoding"
    844 	    return -code error "permission denied"
    845 	}
    846     } else {
    847 	set at 0
    848 	set encoding {}
    849     }
    850     if {$argc != 1} {
    851 	set msg "wrong # args: should be \"source ?-encoding E? fileName\""
    852 	Log $slave "$msg ($args)"
    853 	return -code error $msg
    854     }
    855     set file [lindex $args $at]
    856 
    857     # get the real path from the virtual one.
    858     if {[catch {
    859 	set realfile [TranslatePath $slave $file]
    860     } msg]} {
    861 	Log $slave $msg
    862 	return -code error "permission denied"
    863     }
    864 
    865     # check that the path is in the access path of that slave
    866     if {[catch {
    867 	FileInAccessPath $slave $realfile
    868     } msg]} {
    869 	Log $slave $msg
    870 	return -code error "permission denied"
    871     }
    872 
    873     # do the checks on the filename :
    874     if {[catch {
    875 	CheckFileName $slave $realfile
    876     } msg]} {
    877 	Log $slave "$realfile:$msg"
    878 	return -code error $msg
    879     }
    880 
    881     # Passed all the tests, lets source it. Note that we do this all manually
    882     # because we want to control [info script] in the slave so information
    883     # doesn't leak so much. [Bug 2913625]
    884     set old [::interp eval $slave {info script}]
    885     set replacementMsg "script error"
    886     set code [catch {
    887 	set f [open $realfile]
    888 	fconfigure $f -eofchar \032
    889 	if {$encoding ne ""} {
    890 	    fconfigure $f -encoding $encoding
    891 	}
    892 	set contents [read $f]
    893 	close $f
    894 	::interp eval $slave [list info script $file]
    895     } msg opt]
    896     if {$code == 0} {
    897 	set code [catch {::interp eval $slave $contents} msg opt]
    898 	set replacementMsg $msg
    899     }
    900     catch {interp eval $slave [list info script $old]}
    901     # Note that all non-errors are fine result codes from [source], so we must
    902     # take a little care to do it properly. [Bug 2923613]
    903     if {$code == 1} {
    904 	Log $slave $msg
    905 	return -code error $replacementMsg
    906     }
    907     return -code $code -options $opt $msg
    908 }
    909 
    910 # AliasLoad is the target of the "load" alias in safe interpreters.
    911 
    912 proc ::safe::AliasLoad {slave file args} {
    913     set argc [llength $args]
    914     if {$argc > 2} {
    915 	set msg "load error: too many arguments"
    916 	Log $slave "$msg ($argc) {$file $args}"
    917 	return -code error $msg
    918     }
    919 
    920     # package name (can be empty if file is not).
    921     set package [lindex $args 0]
    922 
    923     namespace upvar ::safe S$slave state
    924 
    925     # Determine where to load. load use a relative interp path and {}
    926     # means self, so we can directly and safely use passed arg.
    927     set target [lindex $args 1]
    928     if {$target ne ""} {
    929 	# we will try to load into a sub sub interp; check that we want to
    930 	# authorize that.
    931 	if {!$state(nestedok)} {
    932 	    Log $slave "loading to a sub interp (nestedok)\
    933 			disabled (trying to load $package to $target)"
    934 	    return -code error "permission denied (nested load)"
    935 	}
    936     }
    937 
    938     # Determine what kind of load is requested
    939     if {$file eq ""} {
    940 	# static package loading
    941 	if {$package eq ""} {
    942 	    set msg "load error: empty filename and no package name"
    943 	    Log $slave $msg
    944 	    return -code error $msg
    945 	}
    946 	if {!$state(staticsok)} {
    947 	    Log $slave "static packages loading disabled\
    948 			(trying to load $package to $target)"
    949 	    return -code error "permission denied (static package)"
    950 	}
    951     } else {
    952 	# file loading
    953 
    954 	# get the real path from the virtual one.
    955 	try {
    956 	    set file [TranslatePath $slave $file]
    957 	} on error msg {
    958 	    Log $slave $msg
    959 	    return -code error "permission denied"
    960 	}
    961 
    962 	# check the translated path
    963 	try {
    964 	    FileInAccessPath $slave $file
    965 	} on error msg {
    966 	    Log $slave $msg
    967 	    return -code error "permission denied (path)"
    968 	}
    969     }
    970 
    971     try {
    972 	return [::interp invokehidden $slave load $file $package $target]
    973     } on error msg {
    974 	Log $slave $msg
    975 	return -code error $msg
    976     }
    977 }
    978 
    979 # FileInAccessPath raises an error if the file is not found in the list of
    980 # directories contained in the (master side recorded) slave's access path.
    981 
    982 # the security here relies on "file dirname" answering the proper
    983 # result... needs checking ?
    984 proc ::safe::FileInAccessPath {slave file} {
    985     namespace upvar ::safe S$slave state
    986     set access_path $state(access_path)
    987 
    988     if {[file isdirectory $file]} {
    989 	return -code error "\"$file\": is a directory"
    990     }
    991     set parent [file dirname $file]
    992 
    993     # Normalize paths for comparison since lsearch knows nothing of
    994     # potential pathname anomalies.
    995     set norm_parent [file normalize $parent]
    996 
    997     namespace upvar ::safe S$slave state
    998     if {$norm_parent ni $state(access_path,norm)} {
    999 	return -code error "\"$file\": not in access_path"
   1000     }
   1001 }
   1002 
   1003 proc ::safe::DirInAccessPath {slave dir} {
   1004     namespace upvar ::safe S$slave state
   1005     set access_path $state(access_path)
   1006 
   1007     if {[file isfile $dir]} {
   1008 	return -code error "\"$dir\": is a file"
   1009     }
   1010 
   1011     # Normalize paths for comparison since lsearch knows nothing of
   1012     # potential pathname anomalies.
   1013     set norm_dir [file normalize $dir]
   1014 
   1015     namespace upvar ::safe S$slave state
   1016     if {$norm_dir ni $state(access_path,norm)} {
   1017 	return -code error "\"$dir\": not in access_path"
   1018     }
   1019 }
   1020 
   1021 # This procedure is used to report an attempt to use an unsafe member of an
   1022 # ensemble command.
   1023 
   1024 proc ::safe::BadSubcommand {slave command subcommand args} {
   1025     set msg "not allowed to invoke subcommand $subcommand of $command"
   1026     Log $slave $msg
   1027     return -code error -errorcode {TCL SAFE SUBCOMMAND} $msg
   1028 }
   1029 
   1030 # AliasEncoding is the target of the "encoding" alias in safe interpreters.
   1031 
   1032 proc ::safe::AliasEncoding {slave option args} {
   1033     # Note that [encoding dirs] is not supported in safe slaves at all
   1034     set subcommands {convertfrom convertto names system}
   1035     try {
   1036 	set option [tcl::prefix match -error [list -level 1 -errorcode \
   1037 		[list TCL LOOKUP INDEX option $option]] $subcommands $option]
   1038 	# Special case: [encoding system] ok, but [encoding system foo] not
   1039 	if {$option eq "system" && [llength $args]} {
   1040 	    return -code error -errorcode {TCL WRONGARGS} \
   1041 		"wrong # args: should be \"encoding system\""
   1042 	}
   1043     } on error {msg options} {
   1044 	Log $slave $msg
   1045 	return -options $options $msg
   1046     }
   1047     tailcall ::interp invokehidden $slave encoding $option {*}$args
   1048 }
   1049 
   1050 # Various minor hiding of platform features. [Bug 2913625]
   1051 
   1052 proc ::safe::AliasExeName {slave} {
   1053     return ""
   1054 }
   1055 
   1056 proc ::safe::Setup {} {
   1057     ####
   1058     #
   1059     # Setup the arguments parsing
   1060     #
   1061     ####
   1062 
   1063     # Share the descriptions
   1064     set temp [::tcl::OptKeyRegister {
   1065 	{-accessPath -list {} "access path for the slave"}
   1066 	{-noStatics "prevent loading of statically linked pkgs"}
   1067 	{-statics true "loading of statically linked pkgs"}
   1068 	{-nestedLoadOk "allow nested loading"}
   1069 	{-nested false "nested loading"}
   1070 	{-deleteHook -script {} "delete hook"}
   1071     }]
   1072 
   1073     # create case (slave is optional)
   1074     ::tcl::OptKeyRegister {
   1075 	{?slave? -name {} "name of the slave (optional)"}
   1076     } ::safe::interpCreate
   1077 
   1078     # adding the flags sub programs to the command program (relying on Opt's
   1079     # internal implementation details)
   1080     lappend ::tcl::OptDesc(::safe::interpCreate) $::tcl::OptDesc($temp)
   1081 
   1082     # init and configure (slave is needed)
   1083     ::tcl::OptKeyRegister {
   1084 	{slave -name {} "name of the slave"}
   1085     } ::safe::interpIC
   1086 
   1087     # adding the flags sub programs to the command program (relying on Opt's
   1088     # internal implementation details)
   1089     lappend ::tcl::OptDesc(::safe::interpIC) $::tcl::OptDesc($temp)
   1090 
   1091     # temp not needed anymore
   1092     ::tcl::OptKeyDelete $temp
   1093 
   1094     ####
   1095     #
   1096     # Default: No logging.
   1097     #
   1098     ####
   1099 
   1100     setLogCmd {}
   1101 
   1102     # Log eventually.
   1103     # To enable error logging, set Log to {puts stderr} for instance,
   1104     # via setLogCmd.
   1105     return
   1106 }
   1107 
   1108 namespace eval ::safe {
   1109     # internal variables
   1110 
   1111     # Log command, set via 'setLogCmd'. Logging is disabled when empty.
   1112     variable Log {}
   1113 
   1114     # The package maintains a state array per slave interp under its
   1115     # control. The name of this array is S<interp-name>. This array is
   1116     # brought into scope where needed, using 'namespace upvar'. The S
   1117     # prefix is used to avoid that a slave interp called "Log" smashes
   1118     # the "Log" variable.
   1119     #
   1120     # The array's elements are:
   1121     #
   1122     # access_path       : List of paths accessible to the slave.
   1123     # access_path,norm  : Ditto, in normalized form.
   1124     # access_path,slave : Ditto, as the path tokens as seen by the slave.
   1125     # access_path,map   : dict ( token -> path )
   1126     # access_path,remap : dict ( path -> token )
   1127     # tm_path_slave     : List of TM root directories, as tokens seen by the slave.
   1128     # staticsok         : Value of option -statics
   1129     # nestedok          : Value of option -nested
   1130     # cleanupHook       : Value of option -deleteHook
   1131 }
   1132 
   1133 ::safe::Setup