[Top] [Prev] [Next] [Bottom]

16

16 Safe Tcl and Multiple Interpreters

This chapter describes how to create more than one Tcl interpreter in your application. A child interpreter can be made safe so it can execute untrusted scripts without compromising your application. Command aliases, hidden commands and shared I/O channels enable communication among interpreters. Tcl commands: interp.This feature first appeared in Tcl 7.5, and some some revisions in Tcl 7.6 and 7.7.
Safe Tcl was invented by Nataniel Borenstein and Marshall Rose so they could send Tcl scripts via email and have the recipient safely execute the script without worry of viruses or other attacks. Safe Tcl works by removing dangerous commands like exec and open that would let an untrusted script damage the host computer. You can think of this restricted interpreter as a "padded cell" in which it is safe to execute untrusted scripts. To continue the analogy, if the untrusted code wants to do anything potentially unsafe, it must ask permission. This works by adding additional commands, or aliases, that are implemented by a different Tcl interpreter. For example, a safeopen command could be implemented by limiting file space to a temporary directory that is deleted when the untrusted code terminates.

The key concept of Safe Tcl is that there are two Tcl interpreters in the application, a trusted one and an untrusted (or "safe") one. The trusted interpreter can do anything, and it is used for the main application (e.g., the web browser or email user interface). When the main application receives a message containing an untrusted script, it evalutates that script in the context of the untrusted interpreter. The restricted nature of the untrusted interpreter means the application is safe from attack. This model is much like user-mode and kernel-mode in a multi-user operating system like UNIX or Windows/NT. In these systems, applications run in user mode and trap into the kernel to access resources like files and the network. The kernel implements access controls so that users cannot read and write each others files, or hijack network services.

The dual interpreter model of Safe Tcl has been generalised in Tcl 7.5 and made accesible to Tcl scripts. A Tcl script can create other interpreters, destroy them, create command aliases among them, share I/O channels among them, and evaluate scripts in them.

The interp Command

The interp command is used to create and manipulate interpreters. The interpreter being created is called a slave, and the interpreter that creates it is called the master. The master has complete control over the slave. The interp command is summarized in Table 16-1.
The interp command.
interp aliases slave List aliases that are defined in slave.
interp alias slave cmd1 Return the target command and arguments for the alias cmd1 in slave.
interp alias slave cmd1 master cmd2 arg ... Define cmd1 in slave that is an alias to cmd2 in master with additional args.
interp create ?-safe? slave Create an interpreter named slave.
interp delete slave Destroy interpreter slave.
interp eval slave cmd args ... Evaluate cmd and args in slave.
interp exists slave Returns 1 if slave is an interpreter, else 0.
interp expose slave cmd Expose hidden command cmd in slave.
interp hide slave cmd Hide cmd from slave.
interp invokehidden slave cmd arg ... Invoke hidden command cmd and args in slave.
interp issafe slave Returns 1 if slave was created with -safe flag.
interp share master file slave Share the I/O descriptor named file in master with slave.
interp slaves master Return the list of slave interpreters of master.
interp target slave cmd Return the name of the interpreter that is the target of alias cmd in slave.
interp transfer master file slave Transfer the I/O descriptor named file from master to slave.

Creating Interpreters

Here is a simple example that creates an interpreter, evaluates a couple of commands in it, and then deletes the interpreter:

Creating and deleting an interpreter.
interp create foo
=> foo
interp eval foo {set a 5}
=> 5
set sum [interp eval foo {expr $a + $a}]
=> 10
interp delete foo
In Example 16-1 the interpreter is named foo. Two commands are evaluated in the foo interpreter:

set a 5
expr $a + $a
Note that curly braces are used to protect the commands from any interpretation by the main interpreter. The variable a is defined in the foo interpreter and does not conflict with variables in the main interpreter. The set of variables and procedures in each interpreter is completely independent. Example X shows how to link variables between interpreters.

The Interpreter Hierarchy

A slave interpreter can itself create interpreters, resulting in a hierarchy. The next examples illustrates this, and it shows how the grandparent of an interpreter can reference the grandchild by name. The example uses interp slaves to query the existence of child interpreters.

Creating a hierarchy of interpreters.
interp create foo
=> foo
interp eval foo {interp create bar}
=> bar
interp create {foo bar2}
=> foo bar2
interp slaves
=> foo
interp slaves foo
=> bar bar2
interp delete bar

=> interpreter named "bar" not found
interp delete {foo bar}
The example creates foo, and then it creates two children of foo. The first one is created by foo with this command:

interp eval foo {interp create bar}
The second child is created by the main interpreter. In this case the grandchild must be named by a two-element list to indicate it is a child of a child. The same naming convention is used when the grandchild is deleted:

interp create {foo bar2}
interp delete {foo bar2}
The interp slaves operation returns the names of child (i.e., slave) interpreters. The names are relative to their parent, so the slaves of foo are reported simply as bar and bar2. The name for the current interpreter is the empty list, or {}. This is useful in command aliases and file sharing described later. For security reasons, it is not possible to name the master interpreter from within the slave.

The Interpreter Name as a Command

After interpreter slave is created, a new command is available in the main interpreter, also called slave, that operates on the child interpreter. The following two forms are equivalent, but only for the aliases, alias, eval, and issafe operations:

slave operation args ...
interp operation slave args ...
For example, the following are equivalent commands:

foo eval {set a 5}
interp eval foo {set a 5}
And so are these:

foo issafe
interp issafe foo
However, the operations delete, exists, hide, expose, share, and transfer cannot be used this way. For example, there is no foo delete operation: you must use interp delete foo. If you have a deep hierarchy of interpreters, remember that the command corresponding to the slave is only defined in the parent. For example, if a master creates foo, and foo creates bar, then the master must operate on bar with the interp command. There would not be a foo\ bar command defined in the master. With all these exceptions, you may find it easier to just remember how to use the interp command.

Use list with eval

The interp eval command treats its areguments like eval. If there are extra arguments they are all concatenated together first. This can lose important structure as described in Chapter 10. To be safe, use list to construct your commands. For example, to safely define a variable in the slave, you should do this:

interp eval slave [list set var $value]

Command Aliases

A command alias is a command in one interpreter that is implemented by a command in another interpreter. The master interpreter installs command aliases in its slaves. The command to create an alias has the following general form:

interp alias slave cmd1 target cmd2 ?arg arg ...?
This creates cmd1 in slave that is an alias for cmd2 in target. When cmd1 is invoked in slave, cmd2 is invoked in target. The alias mechanism is transparent to the slave. Whatever cmd2 returns, the slave sees as the return value of cmd1. If cmd2 raises and error, the error is propagated to the slave.

If target is the current interpreter, name it with {}. The empty list is the way to name yourself as the interpreter. This is the most common case, although target could be a different slave. The slave and target can even be the same interpreter.

The arguments to cmd1 are passed to cmd2, after any additional arguments to cmd2 that were specified when the alias was created. These hidden arguments provide a safe way to pass extra arguments to an alias. For example, it is quite common to pass the name of the slave to the alias. In Example 16-3, exit in the interpreter foo is an alias that is implemented in the current interpreter (i.e., {}). When the slave executes exit, the master executes:

interp delete foo
A command alias for exit.
interp create foo
interp alias foo exit {} interp delete foo
interp eval foo exit
# Child foo is gone.

Alias Introspection

You can query what aliases are defined for a child interpreter. The interp aliases command lists the aliases, the interp alias command can also return the value of an alias, and the interp target command tells you what interpreter implements an alias. These are illustrated in the following examples:

Querying aliases.
proc Interp_ListAliases {name out} {
	puts $out "Aliases for $name"
	foreach alias [interp aliases $name] {
		puts $out [format "%-20s => (%s) %s" $alias \
				[interp target $name $alias] \
				[interp alias $name $alias]]
	}
}
Example 16-4 generates output in a human readable format. Example 16-5 generates the aliases as Tcl commands that can be used to recreate them later:

Dumping aliases as Tcl commands.
proc Interp_DumpAliases {name out} {
	puts $out "# Aliases for $name"
	foreach alias [interp aliases $name] {
		puts $out [format "interp alias %s %s %s %s" \
			$name $alias [list [interp target $name $alias]] \
			[interp alias $name $alias]]
	}
}

Safe Interpreters

A child can be created either safe (i.e., untrusted) or fully functional. In the examples so far, the children have been trusted and fully functional; they have all the basic Tcl commands available to them. To create a safe interpreter, use the -safe flag:

interp create -safe untrusted
An interpreter is made safe by eliminating commands. Table 16-2 lists the commands removed from safe interpreters. It does not have commands to manipulate the file system and other programs (e.g., cd, open, and exec). This ensures that untrusted scripts cannot harm the host computer. The socket command is removed so untrusted scripts cannot access the network. The exit, source, and load commands are removed so an untrusted script cannot harm the hosting application. Note that commands like puts and gets are not removed. A safe interpreter can still do I/O, but it cannot create an I/O channel. We will show how to pass an I/O channel to a child interpreter on page 177.
Commands removed from safe interpreters.
cd Change directory.
exec Execute another program.
exit Terminate the process.
fconfigure Set modes of an I/O stream.
file Query file attributes.
glob Pattern match on file names.
load Dynamically load object code.
open Open files and process pipelines.
pwd Determine the current directory.
socket Open network sockets.
source Load scripts.

In addition, a safe interpreter does not have any standard procedures from the Tcl script library. For example, there is no implementation of unknown to automatically load library procedures and packages. Libraries, packages, and the unknown command are described in Chapter 12. Restoring these functions in a slave is described in Example 16-6 on page 176.

The initial state of a safe interpreter is very safe, but it is too limited. The only thing a safe interpreter can do is compute a string and return that value to the parent. By creating command aliases, a master can give a safe interpreter controlled access to resources. A security policy implements a set of command aliases that add controlled capabilities to a safe interpreter. We willshow, for example, how to provided limited network and file system access to untrusted slaves.

Hidden Commands

The commands listed in Table 16-2 are hidden instead of being completely removed. A hidden command can be invoked in a slave by its master. For example, a master can load Tcl scripts into a slave by using its hidden source command:

interp create -safe slave
interp invokehidden slave source filename
Without hidden commands the master has to do a bit more work to achieve the same thing. It must open and read the file and eval the contents of the file in the slave. File operations are described in Chapter 9.

interp create -safe slave
set in [open filename]
interp eval slave [read $in]
close $in
Hidden commands were added in Tcl 7.7 in order to better support the Tcl/Tk browser plugin described in Chapter 19. In some cases hidden commands are strictly necessary; it is not possible to simulate them any other way. The best examples are in the context of Safe Tk, where the master creates widgets or does potentially dangerous things on behalf of the slave. These will be discussed in more detail later.

A master can hide and expose commands using the interp hide and interp expose operations, respectively. You can even hide Tcl procedures. However, the commands inside the procedure run with the same privilege as the slave.

For example, if you are really paranoid you might not want an untrusted interpreter to read the clock or get timing information. You can hide the clock and time commands:

interp create -safe slave
interp hide slave clock
interp hide slave time
You can remove them from the slave entirely like this:

interp eval slave [list rename clock {}]
interp eval slave [list rename time {}]
On the otherhand, you might want to let slaves source scripts and use the unknown facility that automatically loads scripts. It can be argued that this is safe: even if an untrusted script loaded scripts that used dangerous commands, those commands would not be accessible to the slave. Attempts to use them would fail. However, by letting a slave use source, the slave can learn some things about your file system. It is up to you to decide if this risk is worth it. Example 16-6 gives unrestricted access to source, and it initializes the unknown command and package loading facilities by sourcing the standard init.tcl script. A more restricted solution is presented in Example X.:

Letting a slave use source and auto loading.
proc Interp_AllowSource {slave} {
	interp expose $slave source
	interp expose $slave load
	foreach varName {tcl_library auto_path} {
		upvar #0 $varName var
		interp eval $slave [list set $varName $var]
	}
	interp eval $slave \
		[list source [file join $tcl_library init.tcl]]
}

Substitutions

You must be aware of Tcl parsing and substitutions when commands are invoked in other interpreters. There are three cases corresponding to interp eval, interp invokehidden, and command aliases:

With interp eval the command is subject to a complete round of parsing and substitutions in the target interpreter. This occurs after the parsing and substitutions for the interp eval command itself. In addition, if you pass several arguments to interp eval, those are concatenated before evaluation. This is similar to the way the eval command works as described on page 101. The most reliable way to use interp eval is to construct a list to ensure the command is well structured:

interp eval slave [list cmd arg1 arg2]
With hidden commands, the command and arguments are taken directly from the arguments to interp invokehidden , and there are no substitutions done in the target interpreter. This means that the master has complete control over the command structure, and nothing funny can happen in the other interpreter. For this reason you should not create a list. If you do that, the whole list will be interpreted as the command name! Instead, just pass separate arguments to interp invokehidden and they are passed straight through to the target:

interp invokehidden slave command arg1 arg2
With aliases, all the parsing and substitutions occur in the slave before the alias is invoked in the master. The alias implementation should never eval or subst any values it gets from the slave to avoid executing arbitrary code.

For example, suppose there is an alias to open files. The alias does some checking and then invokes the hidden open command. An untrusted script might pass [exit] as the name of the file to open in order to create mischief. The untrusted code is hoping that the master will accidentally eval the filename and cause the application to exit. This attack has nothing to do with opening files, it just hopes for a poor alias implementation. Example 16-7 shows an alias that is not subject to this attack:

Substitutions and hidden commands.
interp alias slave open {} safeopen slave
proc safeopen {slave filename {mode r}} {

	# do some checks, then...
	interp invokehidden $slave open $filename $mode
}
interp eval slave {open \[exit\]}
# Opens file named "[exit]"
The command in the slave starts out as:

open \[exit\]
The master has to quote the brackets in its interp eval command or else the slave will try to invoke exit because of command substitution. Presumably exit isn't defined, or it is defined to terminate the slave. Once this quoting is done, the value of filename is [exit] and it is not subject to substitutions. It is safe to use $filename in the interp invokehidden command because it is only substituted once, in the master. The hidden open command also gets [exit] as its filename argument.

I/O From Safe Interpreters

A safe child interpreter cannot open files or network sockets directly. However, the parent of a safe interpreter can create an I/O channel (i.e., open a file or socket) and give the child access to it. The parent can share the I/O channel with the child, or it can transfer the I/O channel to the child. If the channel is shared, both the parent and the child can use it. If the channel is transferred, the parent no longer has access to the channel. In general, transferring an I/O channel is simpler, but sharing an I/O channel gives the parent more control over an unsafe child. The differences are illustrated in Example 16-8 and Example 16-10.

There are three properties of I/O channels that are important to consider when choosing between sharing and transferring: the name, the seek offset, and the reference count.

interp share interp1 chanName interp2
interp transfer interp1 chanName interp2
In these commands, chanName exists in interp1 and is being shared or transferred to interp2. As with command aliases, if interp1 is the current interpreter, name it with {}.

The following example creates a temporary file for an unsafe interpreter. The file is opened for reading and writing, and the slave can use it to store data temporarily.

Opening a file for an unsafe interpreter.
proc TempfileAlias {slave} {
	set i 0
	while {[file exists Temp$slave$i]} {
		incr i
	}
	set out [open Temp$slave$i w+]
	interp transfer {} $out $slave
	return $out
}
proc TempfileExitAlias {slave} {
	foreach file [glob -nocomplain Temp$slave*] {
		# Platform independent file remove.
		# This is part of Tcl 7.6.
		file delete $file
	}
	interp delete $slave
}
interp create -safe foo
interp alias foo Tempfile {} TempfileAlias foo
interp alias foo exit {} TempFileExitAlias foo
The TempfileAlias procedure is invoked in the parent when the child interpreter invokes Tempfile. TempfileAlias returns the name of the open channel, and this becomes the return value from Tempfile so the child knows the name of the I/O channel. TempfileAlias uses interp transfer to pass the I/O channel to the child so the child has permission to access the I/O channel. In this simple example, it would also work to invoke the hidden open command to create the I/O channel directly in the slave.

Example 16-8 is not fully safe because the unsafe interpreter can still overflow the disk or create a million files. Because the parent has transferred the I/O channel to the child, it cannot easily monitor the I/O activity by the child. Example 16-10 addresses these issues.

Security Policies

A security policy defines what a safe interpreter can do. Designing security policies that are secure is difficult. If you design your own, make sure to have your colleges review the code. Give out prizes to folks that can break your policy. Good policy implementations are proven with lots of review and trial attacks. The good news is that Safe Tcl security policies can be implemented in relatively small amounts of Tcl code. This makes them easier to analyze and get correct. Here are a number of rules of thumb:

Limited Socket Access

The Safesock security policy provides limited socket access. The policy is designed around a simple table of allowed hosts and ports. An untrusted interpreter can only connect to addresses listed in the table. For example, I would never let untrusted code connect to the sendmail, ftp, or telnet ports on my hosts. There are just too many attacks possible on these ports. On the otherhand, I might want to let untrusted code fetch aURL from certain hosts, or connect to a database server for an intranet application. The goal of this policy is to have a simple way to specify exactly what hosts and ports a slave can access. Here is the code:

The Safesock security policy.
# The index is a host name, and the
# value is a list of port specifications, which can be
# an exact port number
# a lower bound on port number: N-
# a range of port numbers, inclusive: N-M
array set safesock {
	sage.eng				3000-4000
	www.sun.com				80
	webcache.eng				{80 8080}
	bisque.eng				80 1025-
}
proc Safesock_PolicyInit {slave} {
	interp alias $slave socket {} SafesockAlias $slave
}
proc SafesockAlias {slave host port} {
	global safesock
	if ![info exists safesock($host)] {
		error "unknown host: $host"
	}
	foreach portspec $safesock($host) {
		set low [set high ""]
		if {[regexp {^([0-9]+)-([0-9]*)$ $portspec x low 
high]} {
			if {($low <= $port && $high == "") ||
					($low <= $port && $high >= $port)} {
				set good $port
				break
			}
		} elseif {$port == $portspec} {
			set good $port
		}
	}
	if [info exists goodport] {
		set sock [interp invokehidden $slave socket $host 
$good]
		interp invokehidden $slave fconfigure $sock -blocking 
0
		return $sock
	}
	error "bad port: $host"
}
The policy is initialized with Safesock_PolicyInit. The name of this procedure follows a naming convention used by the Tcl/Tk browser plugin. In the plugin, untrusted scripts request a policy by name (e.g., Safesock) and the plugin implementation calls Safesock_PolicyInit to initialize the security policy. In this case, a single alias is installed. The alias gives the slave a socket command that is implemented by SafesockAlias in the master.

The alias checks for a port that matches one of the port specifications for the host. If a match is found then the invokehidden operation is used to invoke two commands in the slave. The socket command creates the network connection, and the fconfigure command puts the socket into non-blocking mode so read and gets by the slave do not block the application:

set sock [interp invokehidden $slave socket $host $good]
interp invokehidden $slave fconfigure $sock -blocking 0
The socket alias in the slave does not conflict with the hidden socket command. There are two distinct sets of commands, hidden and exposed. It is quite common for the alias implementation to invoke the hidden command after various permission checks are made.

Limited Temporary Files

Example 16-10 improves on Example 16-8 by limiting the number of temporary files and the size of the files. It also lets the child specify a file by name, yet it limits the files to a single directory. The example demonstrates a shared I/O channel that gives the master control over output. The file size limit is implemented by an alias for puts. By sharing the I/O channel for the temporary file the slave can use commands like gets, eof, and close, while the master does the puts.

Opening a file for an unsafe interpreter.
proc Tempfile_PolicyInit {slave directory maxfile maxsize} {
	# directory is the location for the files
	# maxfile is the number of files allowed in the directory
	# maxsize is the max size for any single file.

	interp alias $slave open {} \
		TempfileOpenAlias $slave $directory $maxfile
	# Override existing puts with an alias
	interp alias $slave puts {} TempfilePutsAlias $slave 
$maxsize
	interp alias $slave exit {} TempfileExitAlias $slave
}
proc TempfileOpenAlias {slave dir maxfile name {mode r}} {
	# interpState records open files for each child
	global interpState
	# Ignore any leading pathname components
	set name [file join $dir [file tail $name]]
	# Limit the number of files
	set files [glob -nocomplain [file join $dir *]]
	set N [llength $files]
	if {(N >= $maxfile) && ([lsearch -exact $files $name] < 
0)} {
		error "permission denied"
	}
	set out [open $name $mode]
	lappend interpState(channels,$slave) $out
	interp share {} $out $slave
	return $out
}
proc TempfileExitAlias {slave} {
	global interpState
	interp delete $slave
	if [info exists interpState(channels,$slave)] {
		foreach out $interpState(channels,$slave) {
			close $out
		}
	}
}
proc TempfilePutsAlias {slave max chan args} {
	# max is the file size limit, in bytes
	# chan is the I/O channel
	# args is either a single string argument,
	# or the -nonewline flag plus the string.

	if {[llength $args] > 2} {
		error "invalid arguments"
	}
	if {[llength $args] == 2} {
		if {![string match -n* [lindex $argv 0]]} {
			error "invalid arguments"
		}
		set string [lindex $args 1]
	} else {
		set string [lindex $args 0]\n
	}
	set size [expr [tell $chan] + [string length $string]]
	if {$size > $max} {
		error "File size exceeded"
	} else {
		puts -nonewline $chan $string
	}
}
The TempfileAlias procedure is generalized in Example 16-10 to have several parameters that specify the directory, name, and a limit to the number of files allowed. The directory and maxfile limit are part of the alias definition. Their existence is transparent to the slave. The slave only specifies the name and access mode (i.e., for reading or writing.) The Tempfile policy could be used by different slave interreters with different parameters.

The master is careful to restrict the files to the specified directory. It uses file tail to strip off any leading pathname components that the slave might specify.

The TempfilePutsAlias procedure implements a limited form of puts. It checks the size of the file with tell, and measures the output string to see if the total exceeds the limit. The limit comes from a parameter defined when the alias is created. The file cannot grow past the limit, at least not by any action of the child interpreter. The args parameter is used to allow an optional -nonewline flag to puts. The value of args is checked explicitly instead of using the eval trick described in Example 10-2 on page 104. Never eval arguments to aliases or else a slave can attack you with arguments that contain embedded Tcl commands.

The master and slave share the I/O channel. The name of the I/O channel is recorded in interpState, and TempfileExitAlias uses this information to close the channel when the child interpreter is deleted. This is necessary because both parent and child have a reference to the channel when it is shared. The child's reference is automatically removed when the interpreter is deleted, but the parent must close its own reference.

The shared I/O channel lets the master use puts and tell. It is also possible to implement this policy by using hidden puts and tell commands. The reason tell must be hidden is to prevent the slave from implementing its own version of tell that lies about the seek offset value. An additional alias is also needed so the slave can also use tell:

interp alias slave tell {} interp invokehidden tell

Safe after Command

The after command is unsafe because it can pause the application for an arbitrary amount of time. This happens if you only specify a time, but do not specify a command. In this case Tcl just waits for the time period and processes no events. This will stop all interpreters, not just the one doing the after command. This is a kind of resource attack. It doesn't leak information or damage anything, but it disrupts the main application.

Example 16-11 defines an alias that implements after on behalf of safe interpreters. The basic idea is to carefully check the arguments, and then do the after in the parent interpreter. As an additional feature, the number of outstanding after events is limited. The master keeps a record of each after event scheduled. Two id's are associated with each event. One chosen by the master (i.e., myid), and the other chosen by the after command (i.e., id). The master keeps a map from myid to id. The map serves two puposes. The number of map entries counts the number of outstanding events. The map also hides the real after ID from the slave, which prevents a slave from attempting mischief by specifying invalid after IDs to after cancel. The SafeAfterCallback is the procedure scheduled. It maintains state and then invokes the original callback in the slave.

A safe after command.
# SafeAfter_PolicyInit creates a child with 
# a safe after command

proc SafeAfter_PolicyInit {slave max} {
	# max limits the number of outstanding after events
	global interpState
	interp alias $slave after {} SafeAfterAlias $slave $max
	interp alias $slave exit {} SafeAfterExitAlias $slave
	# This is used to generate after IDs for the slave.
	set interpState(id,$slave) 0
}

# SafeAfterAlias is an alias for after. It disallows after
# with only a time argument and no command.

proc SafeAfterAlias {slave max args} {
	global interpState
	set argc [llength $args]
	if {$argc == 0} {
		error "Usage: after option args"
	}
	switch -- [lindex $args 0] {
		cancel {
			# A naive implementation would just
			# eval after cancel $args
			# but something dangerous could be hiding in args.
			set myid [lindex $args 1]
			if {[info exists interpState(id,$slave,$myid)]} {
				set id $interpState(id,$slave,$myid)
				unset interpState(id,$slave,$myid)
				after cancel $id
			}
			return ""
		}
		default {
			if {$argc == 1} {
				error "Usage: after time command args..."
			}
			if {[llength [array names interpState 
id,$slave,*]]\
					>= $max} {
				error "Too many after events"
			}
			# Maintain concat semantics
			set command [concat [lrange $args 1 end]]
			# Compute our own id to pass the callback.
			set myid after#[incr interpState(id,$slave)]
			set id [after [lindex $args 0] \
				[list SafeAfterCallback $slave $myid $command]]
			set interpState(id,$slave,$myid) $id
			return $myid
		}
	}
}

# SafeAfterCallback is the after callback in the trusted 
code.
# It evaluates its command in the safe interpreter.

proc SafeAfterCallback {slave myid cmd} {
	global interpState
	unset interpState(id,$slave,$myid)
	if [catch {
		interp eval $slave $cmd
	} err] {
		catch {interp eval $slave bgerror $error}
	}
}

# SafeAfterExitAlias is an alias for exit that does cleanup.

proc SafeAfterExitAlias {slave} {
	global interpState
	foreach id [array names interpState id,$slave,*] {
		after cancel $interpState($id)
	}
	interp delete $slave
	unset interpState
}


[Top] [Prev] [Next] [Bottom]

welch@acm.org
Copyright © 1996, Brent Welch. All rights reserved.
This will be published by Prentice Hall as the 2nd Edition of
Practical Programming in Tcl and Tk