Spaces:
Running
Running
# 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 | |