File Coverage

blib/lib/App/EvalServer/Child.pm
Criterion Covered Total %
statement 26 76 34.2
branch 0 22 0.0
condition 0 33 0.0
subroutine 10 15 66.6
pod 1 1 100.0
total 37 147 25.1


line stmt bran cond sub pod time code
1             package App::EvalServer::Child;
2             BEGIN {
3 1     1   8213 $App::EvalServer::Child::AUTHORITY = 'cpan:HINRIK';
4             }
5             BEGIN {
6 1     1   22 $App::EvalServer::Child::VERSION = '0.08';
7             }
8              
9 1     1   11 use strict;
  1         2  
  1         41  
10 1     1   85 use warnings FATAL => 'all';
  1         3  
  1         54  
11 1     1   1240 use BSD::Resource;
  1         8215  
  1         5  
12 1     1   1577 use POE::Filter::Reference;
  1         38777  
  1         40  
13 1     1   16 use POSIX qw;
  1         2  
  1         10  
14              
15             # we need to load these here, otherwise they'll be loaded on demand
16             # after the chroot, which will fail
17             getrusage();
18 1     1   396 use Carp::Heavy;
  1         3  
  1         87  
19 1     1   9 use Storable 'nfreeze'; nfreeze([]);
  1         2  
  1         90  
20 1     1   6 use File::Glob;
  1         2  
  1         3683  
21              
22             my $PIPE;
23             my $FILTER;
24              
25             sub run {
26 0     0 1   my ($tempdir, $pipe_name, $jail, $user, $limit, $lang, $code, $unsafe)
27             = @ARGV;
28              
29 0 0         open $PIPE, '>', $pipe_name or die "Can't open $pipe_name: $!";
30 0           $FILTER = POE::Filter::Reference->new();
31              
32             # _Inline directories and such will end up here
33 0 0         chdir $tempdir or _fail("Can't chdir $tempdir: $!");
34              
35 0           my $class = "App::EvalServer::Language::$lang";
36 0           eval "require $class";
37 0           chomp $@;
38 0 0         _fail($@) if $@;
39              
40 0 0         _be_safe($jail, $user, $limit) if !$unsafe;
41              
42             # is this the best approach?
43 0           for my $signal (qw) {
44             $SIG{$signal} = sub {
45 0     0     _fail('Got a fatal signal', { signal => $signal });
46 0           };
47             }
48              
49 0           my $result = $class->evaluate($code);
50 0           my ($user_time, $sys_time, $memory) = _usage();
51 0           my $return = {
52             result => $result,
53             user_time => $user_time,
54             sys_time => $sys_time,
55             memory => $memory,
56             };
57              
58 0           print $PIPE $FILTER->put([$return])->[0];
59 0           exit;
60             }
61              
62             sub _usage {
63 0     0     my $self_usage = [getrusage(RUSAGE_SELF)];
64 0           my $child_usage = [getrusage(RUSAGE_CHILDREN)];
65 0           my $user_time = $self_usage->[0];
66 0           my $sys_time = $self_usage->[1];
67 0           my $memory = $self_usage->[2] + $child_usage->[2];
68              
69 0           return ($user_time, $sys_time, $memory);
70             }
71              
72             sub _fail {
73 0     0     my ($error, $return) = @_;
74 0 0         $return = { } if ref $return ne 'HASH';
75              
76 0           $return->{error} = $error;
77 0           my ($user_time, $sys_time, $memory) = _usage();
78 0           $return->{user_time} = $user_time;
79 0           $return->{sys_time} = $sys_time;
80 0           $return->{memory} = $memory;
81              
82 0           print $PIPE $FILTER->put([$return])->[0];
83 0           exit;
84             }
85              
86             sub _be_safe {
87 0     0     my ($jail, $user, $limit) = @_;
88              
89 0           my $new_uid = getpwnam($user);
90 0 0         _fail("Can't find uid for '$user'") if !defined $new_uid;
91            
92             # Set the CPU LIMIT.
93             # Do this before the chroot because some of the other
94             # setrlimit calls will prevent chroot from working
95             # however at the same time we need to preload an autload file
96             # that chroot will prevent, so do it here.
97 0           setrlimit(RLIMIT_CPU, 10, 10);
98              
99 0 0         _fail("Not root, can't chroot or take other precautions, dying") if $< != 0;
100              
101 0 0         chdir or _fail("Failed to chdir into $jail: $!");
102 0 0         chroot '.' or _fail("Failed to chroot into $jail: $!");
103              
104             # drop root privileges
105 0           $)="$new_uid $new_uid";
106 0           $(=$new_uid;
107 0           $<=$>=$new_uid;
108 0           setgid($new_uid); #We just assume the uid is the same as the gid. Hot.
109              
110 0 0 0       if ($> != $new_uid || $< != $new_uid) {
111 0           _fail("Failed to drop root privileges");
112             }
113              
114 0           my $kilo = 1024;
115 0           my $meg = $kilo * $kilo;
116 0           my $limit_bytes = $limit * $meg;
117              
118             (
119 0 0 0       setrlimit(RLIMIT_DATA, $limit_bytes, $limit_bytes)
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
120             and
121             setrlimit(RLIMIT_STACK, $limit_bytes, $limit_bytes)
122             and
123             setrlimit(RLIMIT_NPROC, 1, 1)
124             and
125             setrlimit(RLIMIT_NOFILE, 0, 0)
126             and
127             setrlimit(RLIMIT_OFILE, 0, 0)
128             and
129             setrlimit(RLIMIT_OPEN_MAX, 0, 0)
130             and
131             setrlimit(RLIMIT_LOCKS, 0, 0)
132             and
133             setrlimit(RLIMIT_AS, $limit_bytes, $limit_bytes)
134             and
135             setrlimit(RLIMIT_VMEM, $limit_bytes, $limit_bytes)
136             and
137             setrlimit(RLIMIT_MEMLOCK, 100, 100)
138             and
139             setrlimit(RLIMIT_CPU, 10, 10)
140             )
141             or _fail("Failed to set resource limits: $!");
142              
143             #setrlimit(RLIMIT_MSGQUEUE,100,100);
144 0           return;
145             }
146              
147             1;
148              
149             =encoding utf8
150              
151             =head1 NAME
152              
153             App::EvalServer::Child - Evaluate code in a safe child process
154              
155             =head1 SYNOPSIS
156              
157             # fork, etc
158              
159             use App::EvalServer::Child;
160             App::EvalServer::Child::run(
161             $tempdir, $pipe_name, $jail, $user, $limit, $lang, $code, $unsafe,
162             );
163              
164             =head1 DESCRIPTION
165              
166             This module takes various safety precautions, then executes the code you
167             provided.
168              
169             =head1 FUNCTIONS
170              
171             =head2 C
172              
173             Runs the code. Takes the following arguments: a temporary directory, a pipe
174             name, a jail path, a username, a process limit (in megabytes), a language
175             suffix (e.g. 'Perl' for C), the code, and
176             an unsafe flag. If the unsafe flag is on, C will not take safety
177             precautions (change user, chroot, set resource limits) which require root
178             access.
179              
180             =head1 AUTHOR
181              
182             Hinrik Ern SigurEsson, hinrik.sig@gmail.com
183              
184             =head1 LICENSE AND COPYRIGHT
185              
186             Copyright 2010 Hinrik Ern SigurEsson
187              
188             This program is free software, you can redistribute it and/or modify
189             it under the same terms as Perl itself.
190              
191             =cut