comparison CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/tcl8.6/safe.tcl @ 68:5028fdace37b

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