In Progress
Unit 1, Lesson 21
In Progress

Subprocesses Part 8: Environmental Isolation

Video transcript & code

In the previous episode we talked about setting resource limits on processes started from Ruby programs. Today we're going to focus on a slightly different kind of process limitation. We're going to learn how to isolate a process from its parent's environment.

We already know that we can modify the environment variables for a subprocess.

For instance, here we have a program that just starts a subprocess which echoes the current user.

Process.spawn "echo Current user is: $USER"

# >> Current user is: avdi

If we pass a hash as the first argument, we can modify the value of the USER environment variable.

Process.spawn({"USER" => "crow"}, "echo Current user is: $USER")

# >> Current user is: crow

When we do this, we only override the value of a single environment variable at a time. All the other environment variables are inherited as-is from the parent process.

This isn't always what we want. Why not? Well, consider the case where we are writing a system automation script.

As part of its code, this script sets up PStore to save program state.

require "pstore"

store = PStore.new(File.expand_path("~/.frob.pstore"))

store.transaction do |s|
  s[:last_run] = Time.now
end

puts "System frobbed!"

We're running the script from our own user account as we are developing and testing it.

And it seems to work fine.

system("./frob")

# >> System frobbed!

But eventually, we'd like the script to be able to run as a cron job or as a system daemon. One thing we know about system processes is that they often run in a very different environment than processes run in the context of a user account.

We're not sure about all the differences. But we do want to make sure that our script will run OK no matter what. So we need a way to test that our script isn't implicitly dependent on some aspect of our user environment.

We need to run our script with all environment variables removed. To do this, we start by passing an empty hash as the first argument to Process.spawn.

Normally, this would have no effect: the child process would just inherit the whole parent environment.

But then, we add a new flag. We set unsetenv_others to true.

This option tells Ruby to unset any environment variables which aren't explicitly specified in the environment hash. Since we haven't specified any variables at all, Ruby runs our subprocess with no environment variables at all.

When we run this, we see a failure.

Because we ran the program without a PATH environment variable, the env command can't find a ruby executable.

system({}, "./frob", unsetenv_others: true)

# !> /usr/bin/env: 'ruby': No such file or directory

This is a common issue when moving a userspace script into the system realm. To work in any environment, scripts need to have a hardcoded interpreter.

require "pstore"

store = PStore.new(File.expand_path("~/.frob.pstore"))

store.transaction do |s|
  s[:last_run] = Time.now
end

puts "System frobbed!"

When we run this again, we get a little further.

system({}, "./frob2", unsetenv_others: true)

# !> ./frob2:11:in `expand_path': couldn't find HOME environment -- expanding `~' (ArgumentError)
# !>    from ./frob2:11:in `<main>'

But we still get an error. This time, we see that there has been a problem expanding a tilde in a file path string.

This is because tilde normally refers to the current user's home directory, and by unsetting all variables, we've gotten rid of both the HOME variable which is normally used to find the home directory.

In order to make our script robust, we have to hardcode the path where it should store its state.

require "pstore"

store = PStore.new("/tmp/frob.pstore")

store.transaction do |s|
  s[:last_run] = Time.now
end

puts "System frobbed!"

When we run this version, it finally works.

system({}, "./frob3", unsetenv_others: true)

# >> System frobbed!

What we've seen here is that Ruby programs are often implicitly dependent on the state of various environment variables. For many applications, this is just fine. But if we ever need to lock down a script's environment—or just audit it to find out what variables it relies on—the unsetenv_others flag can come in handy. Happy hacking!

Responses