summaryrefslogtreecommitdiff
path: root/tcl/library/safeinit.tcl
blob: e1ce1a039599ed999f512dd1d13d5d9a389cd819 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# safeinit.tcl --
#
# This code runs in a master to manage a safe slave with Safe Tcl.
# 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.
#
# SCCS: @(#) safeinit.tcl 1.38 97/06/20 12:57:39

# This procedure creates a safe slave, initializes it with the
# safe base and installs the aliases for the security policy mechanism.

proc tcl_safeCreateInterp {slave} {
    global auto_path

    # Create the slave.
    interp create -safe $slave

    # Set its auto_path
    interp eval $slave [list set auto_path $auto_path]

    # And initialize it.
    return [tcl_safeInitInterp $slave]
}

# This procedure applies the initializations to an already existing
# interpreter. It is useful when you want to enable an interpreter
# created with "interp create -safe" to use security policies.

proc tcl_safeInitInterp {slave} {
    upvar #0 tclSafe$slave state
    global tcl_library tk_library auto_path tcl_platform

    # These aliases let the slave load files to define new commands

    interp alias $slave source {} tclSafeAliasSource $slave
    interp alias $slave load {} tclSafeAliasLoad $slave

    # This alias lets the slave have access to a subset of the 'file'
    # command functionality.
    tclAliasSubset $slave file file dir.* join root.* ext.* tail \
	path.* split

    # This alias interposes on the 'exit' command and cleanly terminates
    # the slave.
    interp alias $slave exit {} tcl_safeDeleteInterp $slave

    # Source init.tcl into the slave, to get auto_load and other
    # procedures defined:

    if {$tcl_platform(platform) == "macintosh"} {
	if {[catch {interp eval $slave [list source -rsrc Init]}]} {
	    if {[catch {interp eval $slave \
			[list source [file join $tcl_library init.tcl]]}]} {
		error "can't source init.tcl into slave $slave"
	    }
	}
    } else {
	if {[catch {interp eval $slave \
			[list source [file join $tcl_library init.tcl]]}]} {
	    error "can't source init.tcl into slave $slave"
	}
    }

    # Loading packages into slaves is handled by their master.
    # This is overloaded to deal with regular packages and security policies

    interp alias $slave tclPkgUnknown {} tclSafeAliasPkgUnknown $slave
    interp eval $slave {package unknown tclPkgUnknown}

    # We need a helper procedure to define a $dir variable and then
    # do a source of the pkgIndex.tcl file
    interp eval $slave \
	[list proc tclPkgSource {dir args} {
		if {[llength $args] == 2} {
		    source [lindex $args 0] [lindex $args 1]
		} else {
		    source [lindex $args 0]
		}
	      }]

    # Let the slave inherit a few variables
    foreach varName \
	{tcl_library tcl_version tcl_patchLevel \
	 tcl_platform(platform) auto_path} {
	upvar #0 $varName var
	interp eval $slave [list set $varName $var]
    }

    # Other variables are predefined with set values
    foreach {varName value} {
	    auto_noexec 1
	    errorCode {}
	    errorInfo {}
	    env() {}
	    argv0 {}
	    argv {}
	    argc 0
	    tcl_interactive 0
	    } {
	interp eval $slave [list set $varName $value]
    }

    # If auto_path is not set in the slave, set it to empty so it has
    # a value and exists. Otherwise auto_loading and package require
    # will complain.

    interp eval $slave {
	if {![info exists auto_path]} {
	    set auto_path {}
	}
    }

    # If we have Tk, make the slave have the same library as us:

    if {[info exists tk_library]} {
        interp eval $slave [list set tk_library $tk_library]
    }

    # Stub out auto-exec mechanism in slave
    interp eval $slave [list proc auto_execok {name} {return {}}]

    return $slave
}

# This procedure deletes a safe slave managed by Safe Tcl and
# cleans up associated state:

proc tcl_safeDeleteInterp {slave args} {
    upvar #0 tclSafe$slave state

    # If the slave has a policy loaded, clean it up now.
    if {[info exists state(policyLoaded)]} {
	set policy $state(policyLoaded)
	set proc ${policy}_PolicyCleanup
	if {[string compare [info proc $proc] $proc] == 0} {
	    $proc $slave
	}
    }

    # Discard the global array of state associated with the slave, and
    # delete the interpreter.
    catch {unset state}
    catch {interp delete $slave}

    return
}

# This procedure computes the global security policy search path.

proc tclSafeComputePolicyPath {} {
    global auto_path tclSafeAutoPathComputed tclSafePolicyPath

    set recompute 0
    if {(![info exists tclSafePolicyPath]) ||
	    ("$tclSafePolicyPath" == "")} {
	set tclSafePolicyPath ""
	set tclSafeAutoPathComputed ""
	set recompute 1
    }
    if {"$tclSafeAutoPathComputed" != "$auto_path"} {
	set recompute 1
	set tclSafeAutoPathComputed $auto_path
    }
    if {$recompute == 1} {
	set tclSafePolicyPath ""
	foreach i $auto_path {
	    lappend tclSafePolicyPath [file join $i policies]
	}
    }
    return $tclSafePolicyPath
}

# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------

# tclSafeAliasSource is the target of the "source" alias in safe interpreters.

proc tclSafeAliasSource {slave args} {
    global auto_path errorCode errorInfo

    if {[llength $args] == 2} {
	if {[string compare "-rsrc" [lindex $args 0]] != 0} {
	    return -code error "incorrect arguments to source"
	}
	if {[catch {interp invokehidden $slave source -rsrc [lindex $args 1]} \
		 msg]} {
	    return -code error $msg
	}
    } else {
	set file [lindex $args 0]
	if {[catch {tclFileInPath $file $auto_path $slave} msg]} {
	    return -code error "permission denied"
	}
	set errorInfo ""
	if {[catch {interp invokehidden $slave source $file} msg]} {
	    return -code error $msg
	}
    }
    return $msg
}

# tclSafeAliasLoad is the target of the "load" alias in safe interpreters.

proc tclSafeAliasLoad {slave file args} {
    global auto_path

    if {[llength $args] == 2} {
	# Trying to load into another interpreter
	# Allow this for a child of the slave, or itself
	set other [lindex $args 1]
	foreach x $slave y $other {
	    if {[string length $x] == 0} {
		break
	    } elseif {[string compare $x $y] != 0} {
		return -code error "permission denied"
	    }
	}
	set slave $other
    }

    if {[string length $file] && \
		[catch {tclFileInPath $file $auto_path $slave} msg]} {
	return -code error "permission denied"
    }
    if {[catch {
	switch [llength $args] {
	    0 {
		interp invokehidden $slave load $file
	    }
	    1 -
	    2 {
		interp invokehidden $slave load $file [lindex $args 0]
	    }
	    default {
		error "too many arguments to load"
	    }
	}
    } msg]} {
	return -code error $msg
    }
    return $msg
}

# tclFileInPath raises an error if the file is not found in
# the list of directories contained in path.

proc tclFileInPath {file path slave} {
    set realcheckpath [tclSafeCheckAutoPath $path $slave]
    set pwd [pwd]
    if {[file isdirectory $file]} {
	error "$file: not found"
    }
    set parent [file dirname $file]
    if {[catch {cd $parent} msg]} {
	error "$file: not found"
    }
    set realfilepath [file split [pwd]]
    foreach dir $realcheckpath {
	set match 1
	foreach a [file split $dir] b $realfilepath {
	    if {[string length $a] == 0} {
		break
	    } elseif {[string compare $a $b] != 0} {
		set match 0
		break
	    }
	}
	if {$match} {
	    cd $pwd
	    return 1
	}
    }
    cd $pwd
    error "$file: not found"
}

# This procedure computes our expanded copy of the path, as needed.
# It returns the path after expanding out all aliases.

proc tclSafeCheckAutoPath {path slave} {
    global auto_path
    upvar #0 tclSafe$slave state

    if {![info exists state(expanded_auto_path)]} {
	# Compute for the first time:
	set state(cached_auto_path) $path
    } elseif {"$state(cached_auto_path)" != "$path"} {
	# The value of our path changed, so recompute:
	set state(cached_auto_path) $path
    } else {
	# No change: no need to recompute.
	return $state(expanded_auto_path)
    }

    set pwd [pwd]
    set state(expanded_auto_path) ""
    foreach dir $state(cached_auto_path) {
	if {![catch {cd $dir}]} {
	    lappend state(expanded_auto_path) [pwd]
	}
    }
    cd $pwd
    return $state(expanded_auto_path)
}

proc tclSafeAliasPkgUnknown {slave package version {exact {}}} {
    tclSafeLoadPkg $slave $package $version $exact
}

proc tclSafeLoadPkg {slave package version exact} {
    if {[string length $version] == 0} {
	set version 1.0
    }
    tclSafeLoadPkgInternal $slave $package $version $exact 0
}

proc tclSafeLoadPkgInternal {slave package version exact round} {
    global auto_path
    upvar #0 tclSafe$slave state

    # Search the policy path again; it might have changed in the meantime.

    if {$round == 1} {
	tclSafeResearchPolicyPath

	if {[tclSafeLoadPolicy $slave $package $version]} {
	    return
	}
    }

    # Try to load as a policy.

    if [tclSafeLoadPolicy $slave $package $version] {
	return
    }

    # The package is not a security policy, so do the regular setup.

    # Here we run tclPkgUnknown in the master, but we hijack
    # the source command so the setup ends up happening in the slave.

    rename source source.orig
    proc source {args} "upvar dir dir
	interp eval [list $slave] tclPkgSource \[list \$dir\] \$args"

    if [catch {tclPkgUnknown $package $version $exact} err] {
	global errorInfo

	rename source {}
	rename source.orig source

	error "$err\n$errorInfo"
    }
    rename source {}
    rename source.orig source

    # If we are in the first round, check if the package
    # is now known in the slave:

    if {$round == 0} {
        set ifneeded \
		[interp eval $slave [list package ifneeded $package $version]]

	if {"$ifneeded" == ""} {
	    return [tclSafeLoadPkgInternal $slave $package $version $exact 1]
	}
    }
}

proc tclSafeResearchPolicyPath {} {
    global tclSafePolicyPath auto_index auto_path

    # If there was no change, do not search again.

    if {![info exists tclSafePolicyPath]} {
	set tclSafePolicyPath ""
    }
    set oldPolicyPath $tclSafePolicyPath
    set newPolicyPath [tclSafeComputePolicyPath]
    if {"$newPolicyPath" == "$oldPolicyPath"} {
	return
    }

    # Loop through the path from back to front so early directories
    # end up overriding later directories.  This code is like auto_load,
    # but only new-style tclIndex files (version 2) are supported.

    for {set i [expr [llength $newPolicyPath] - 1]} \
	    {$i >= 0} \
	    {incr i -1} {
	set dir [lindex $newPolicyPath $i]
        set file [file join $dir tclIndex]
	if {[file exists $file]} {
	    if {[catch {source $file} msg]} {
		puts stderr "error sourcing $file: $msg"
	    }
	}
	foreach file [lsort [glob -nocomplain [file join $dir *]]] {
	    if {[file isdir $file]} {
		set dir $file
		set file [file join $file tclIndex]
		if {[file exists $file]} {
		    if {[catch {source $file} msg]} {
			puts stderr "error sourcing $file: $msg"
		    }
		}
	    }
	}
    }
}

proc tclSafeLoadPolicy {slave package version} {
    upvar #0 tclSafe$slave state
    global auto_index

    set proc ${package}_PolicyInit

    if {[info command $proc] == "$proc" ||
	    [info exists auto_index($proc)]} {
	if [info exists state(policyLoaded)] {
	    error "security policy $state(policyLoaded) already loaded"
	}	
	$proc $slave $version
	interp eval $slave [list package provide $package $version]
	set state(policyLoaded) $package
	return 1
    } else {
	return 0
    }
}
# This procedure enables access from a safe interpreter to only a subset of
# the subcommands of a command:

proc tclSafeSubset {command okpat args} {
    set subcommand [lindex $args 0]
    if {[regexp $okpat $subcommand]} {
	return [eval {$command $subcommand} [lrange $args 1 end]]
    }
    error "not allowed to invoke subcommand $subcommand of $command"
}

# This procedure installs an alias in a slave that invokes "safesubset"
# in the master to execute allowed subcommands. It precomputes the pattern
# of allowed subcommands; you can use wildcards in the pattern if you wish
# to allow subcommand abbreviation.
#
# Syntax is: tclAliasSubset slave alias target subcommand1 subcommand2...

proc tclAliasSubset {slave alias target args} {
    set pat ^(; set sep ""
    foreach sub $args {
	append pat $sep$sub
	set sep |
    }
    append pat )\$
    interp alias $slave $alias {} tclSafeSubset $target $pat
}