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