Skip to content

Commit

Permalink
Added stderr > stdout redirection to sandbox options.
Browse files Browse the repository at this point in the history
  • Loading branch information
pwnall committed Dec 20, 2011
1 parent 9794f5c commit fd58843
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 25 deletions.
1 change: 1 addition & 0 deletions lib/exec_sandbox.rb
Expand Up @@ -6,6 +6,7 @@
require 'etc'
require 'fcntl'
require 'fileutils'
require 'set'
require 'tempfile'
require 'tmpdir'

Expand Down
14 changes: 12 additions & 2 deletions lib/exec_sandbox/sandbox.rb
Expand Up @@ -92,8 +92,11 @@ def pull(from, to)
# @option options [String] :out path to a file that is set as the child's
# stdout; if not set, the child will receive the write end of a pipe whose
# contents is returned in :out_data
# @option options [Symbol] :err :none closes the child's stderr, :out
# redirects the child's stderr to stdout; by default, the child's stderr
# is the same as the parent's
# @return [Hash] the result of {Wait4#wait4}, plus an :out_data key if no :out
# option is given
# option is given
def run(command, options = {})
limits = options[:limits] || {}

Expand All @@ -113,7 +116,14 @@ def run(command, options = {})
out_rd, out_wr = IO.pipe
io[:out] = out_wr
end
io[:err] = STDERR unless options[:no_stderr]
case options[:err]
when :out
io[:err] = STDOUT
when :none
# Don't set io[:err], so the child's stderr will be closed.
else
io[:err] = STDERR
end

pid = ExecSandbox::Spawn.spawn command, io, @principal, limits
# Close the pipe ends that are meant to be used in the child.
Expand Down
38 changes: 25 additions & 13 deletions lib/exec_sandbox/spawn.rb
Expand Up @@ -28,32 +28,44 @@ def self.spawn(command, io = {}, principal = {}, resources = {})
# @param [Hash] io associates file descriptors with IO objects or file paths;
# all file descriptors not covered by io will be closed
def self.limit_io(io)
# Sort the list of redirections by file descriptor number.
redirects = []
[:in, :out, :err].each_with_index do |sym, fd_num|
if target = io.delete(sym)
io[fd_num] = target
if target = io[sym]
redirects << [fd_num, redirects.length, target]
end
end
io.each do |k, v|
if v.respond_to?(:fileno)
if v.fileno != k
LibC.close k
LibC.dup2 v.fileno, k
if k.kind_of? Integer
redirects << [k, redirects.length, v]
end
end

# Perform the redirections.
redirects.sort!
redirects.each do |fd_num, _, target|
if target.respond_to?(:fileno)
# IO stream.
if target.fileno != fd_num
LibC.close fd_num
LibC.dup2 target.fileno, fd_num
end
else
LibC.close k
open_fd = IO.sysopen(v, 'r+')
if open_fd != k
LibC.dup2 open_fd, k
# Filename string.
LibC.close fd_num
open_fd = IO.sysopen(target, 'r+')
if open_fd != fd_num
LibC.dup2 open_fd, fd_num
LibC.close open_fd
end
end
end

# Close all file descriptors.
# Close all file descriptors not in the redirection table.
redirected_fds = Set.new redirects.map(&:first)
max_fd = LibC.getdtablesize
0.upto(max_fd) do |fd|
next if io[fd]
LibC.close fd
LibC.close fd unless redirected_fds.include?(fd)
end
end

Expand Down
57 changes: 47 additions & 10 deletions spec/exec_sandbox/sandbox_spec.rb
Expand Up @@ -8,23 +8,44 @@
@temp_in.close
@temp_out = Tempfile.new 'exec_sandbox_rspec'
@temp_out.close

ExecSandbox.use do |s|
@result = s.run bin_fixture(:duplicate), :in => @temp_in.path,
:out => @temp_out.path
end
end
after do
@temp_in.unlink
@temp_out.unlink
end

it 'should not crash' do
@result[:exit_code].should == 0
describe 'duplicate.rb' do
before do
ExecSandbox.use do |s|
@result = s.run bin_fixture(:duplicate), :in => @temp_in.path,
:out => @temp_out.path
end
end

it 'should not crash' do
@result[:exit_code].should == 0
end

it 'should produce the correct result' do
File.read(@temp_out.path).should == "I/O test\nI/O test\n"
end
end

it 'should produce the correct result' do
File.read(@temp_out.path).should == "I/O test\nI/O test\n"

describe 'count.rb' do
before do
ExecSandbox.use do |s|
@result = s.run [bin_fixture(:count), '9'], :in => @temp_in.path,
:out => @temp_out.path, :err => :out
end
end

it 'should not crash' do
@result[:exit_code].should == 0
end

it 'should produce the correct result' do
File.read(@temp_out.path).should == (1..9).map { |i| "#{i}\n" }.join('')
end
end
end

Expand Down Expand Up @@ -61,6 +82,22 @@
@result[:out_data].should == "S" * buffer_size
end
end

describe 'count.rb' do
before do
ExecSandbox.use do |s|
@result = s.run [bin_fixture(:count), '9'], :err => :out
end
end

it 'should not crash' do
@result[:exit_code].should == 0
end

it 'should produce the correct result' do
@result[:out_data].should == (1..9).map { |i| "#{i}\n" }.join('')
end
end
end


Expand Down
29 changes: 29 additions & 0 deletions spec/exec_sandbox/spawn_spec.rb
Expand Up @@ -69,6 +69,35 @@
@status[:exit_code].should_not == 0
end
end

shared_examples_for 'count.rb' do
it 'should not crash' do
@status[:exit_code].should == 0
end

it 'should write successfully' do
@temp_out.open
begin
@temp_out.read.should == (1..9).map { |i| "#{i}\n" }.join('')
ensure
@temp_out.close
end
end
end

describe 'with file descriptor and stderr redirected to stdout' do
before do
File.open(@temp_in.path, 'r') do |in_io|
File.open(@temp_out.path, 'w') do |out_io|
pid = ExecSandbox::Spawn.spawn [bin_fixture(:count), '9'],
{:in => in_io, :out => out_io, :err => STDOUT}
@status = ExecSandbox::Wait4.wait4 pid
end
end
end

it_behaves_like 'count.rb'
end
end

describe '#spawn principal' do
Expand Down
12 changes: 12 additions & 0 deletions spec/fixtures/count.rb
@@ -0,0 +1,12 @@
#!/usr/bin/env ruby

# Counts from 1 to the first argument, and writes each number on a separate
# line. Odd numbers go to STDOUT, even numbers go to STDERR.
# on both STDOUT and STDERR.

limit = ARGV[0].to_i
1.upto(limit) do |i|
file = (i % 2 == 0) ? STDERR : STDOUT
file.write "#{i}\n"
file.flush
end

0 comments on commit fd58843

Please sign in to comment.