bwrap 1.0.0.pre.alpha5 → 1.0.0.pre.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ #require "deep-cover" if ENV["DEEP_COVER"]
4
+
5
+ require "bwrap/version"
6
+ require "bwrap/args/construct"
7
+ require "bwrap/config"
8
+ require "bwrap/execution"
9
+
10
+ # Executes given command under {https://github.com/containers/bubblewrap bwrap}
11
+ # using given configuration.
12
+ #
13
+ # @see ::Bwrap Bwrap module for usage example
14
+ class Bwrap::Bwrap
15
+ include Bwrap::Execution
16
+
17
+ # @param config [Bwrap::Config] Configuration used to tailor bwrap
18
+ def initialize config
19
+ # TODO: Ensure that costruct.rb and utilities it uses does not enforce Config to be valid object.
20
+ # Create a test for that also. Well, as long as that makes sense. If it doesn’t work, validate
21
+ # Config object to be valid here.
22
+ @config = config
23
+ end
24
+
25
+ # Parses command line arguments given to caller script.
26
+ #
27
+ # @note This method automatically sets output levels according parsed
28
+ # options. It is also possible to set the levels manually using
29
+ # {Bwrap::Output.handle_output_options}, for example with flags
30
+ # parsed with optparse’s `OptionParser`, found in Ruby’s standard
31
+ # library.
32
+ #
33
+ # @warning Requires optimist gem to be installed, which is not a dependency of this gem.
34
+ def parse_command_line_arguments
35
+ options = parse_options
36
+
37
+ Bwrap::Output.handle_output_options options
38
+ end
39
+
40
+ # Binds and executes given command available on running system inside bwrap.
41
+ #
42
+ # If {Config#root} has been set, given executable is scanned for necessary
43
+ # libraries and they are bound inside sandbox. This often reduces amount of
44
+ # global binds, but some miscellaneous things may need custom handling.
45
+ #
46
+ # @see Config#features about binding bigger feature sets to sandbox.
47
+ #
48
+ # Executed command is constructed using configuration passed to constructor.
49
+ # After execution has completed, bwrap will also shut down.
50
+ #
51
+ # @param command [String, Array] Command to be executed inside bwrap along with necessary arguments
52
+ # @see #run_inside_root to execute a command that already is inside sandbox.
53
+ def run command
54
+ construct = Bwrap::Args::Construct.new
55
+ construct.command = command
56
+ construct.config = @config
57
+ bwrap_args = construct.construct_bwrap_args
58
+
59
+ exec_command = [ "bwrap" ]
60
+ exec_command += bwrap_args
61
+ exec_command.append command
62
+ exec_command += @cli_args if @cli_args
63
+
64
+ execute exec_command
65
+
66
+ construct.cleanup
67
+ end
68
+
69
+ # Convenience method to executes a command that is inside bwrap.
70
+ #
71
+ # Given command is expected to already be inside {Config#root}.
72
+ #
73
+ # Calling this method is equivalent to setting {Config#command_inside_root} to `true`.
74
+ #
75
+ # @note This may have a bit unintuitive usage, as most things are checked anyway, so this is not
76
+ # akin to running something inside a chroot, but rather for convenience.
77
+ #
78
+ # @param command [String, Array] Command to be executed inside bwrap along with necessary arguments
79
+ # @see #run to execute a command that needs to be bound to the sandbox.
80
+ def run_inside_root command
81
+ if @config
82
+ config = @config.dup
83
+ config.command_inside_root = true
84
+ else
85
+ config = nil
86
+ end
87
+
88
+ construct = Bwrap::Args::Construct.new
89
+ construct.command = command
90
+ construct.config = config
91
+ bwrap_args = construct.construct_bwrap_args
92
+
93
+ exec_command = [ "bwrap" ]
94
+ exec_command += bwrap_args
95
+ exec_command.append command
96
+ exec_command += @cli_args if @cli_args
97
+
98
+ execute exec_command
99
+
100
+ construct.cleanup
101
+ end
102
+
103
+ # Parses global bwrap flags using Optimist.
104
+ #
105
+ # Sets instance variable `@cli_args`.
106
+ #
107
+ # @warning Requires optimist gem to be installed, which is not a dependency of this gem.
108
+ #
109
+ # @api private
110
+ # @return [Hash] options parsed by `Optimist.options`
111
+ private def parse_options
112
+ begin
113
+ require "optimist"
114
+ rescue LoadError => e
115
+ puts "Failed to load optimist gem. In order to use Bwrap::Bwrap#parse_command_line_arguments, " \
116
+ "ensure optimist gem is present for example in your Gemfile."
117
+ puts
118
+ raise e
119
+ end
120
+
121
+ options = Optimist.options do
122
+ version ::Bwrap::VERSION
123
+
124
+ banner "Usage:"
125
+ banner " #{$PROGRAM_NAME} [global options]\n \n"
126
+ banner "Global options:"
127
+ opt :verbose,
128
+ "Show verbose output",
129
+ short: "v"
130
+ opt :debug,
131
+ "Show debug output (useful when debugging a problem)",
132
+ short: "d"
133
+ opt :trace,
134
+ "Show trace output (noisiest, probably not useful for most of time)",
135
+ short: :none
136
+ opt :version,
137
+ "Print version and exit",
138
+ short: "V"
139
+ opt :help,
140
+ "Show help message",
141
+ short: "h"
142
+
143
+ educate_on_error
144
+ end
145
+
146
+ @cli_args = ARGV.dup
147
+
148
+ options
149
+ end
150
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Bwrap::Config
4
+ # Methods to enable or disable feature sets to control various aspects of sandboxing.
5
+ class Features
6
+ # Defines Bash feature set.
7
+ class Bash
8
+ def enabled?
9
+ @enabled
10
+ end
11
+
12
+ def enable
13
+ @enabled = true
14
+ end
15
+
16
+ # Disable Bash feature set.
17
+ def disable
18
+ @enabled = false
19
+ end
20
+ end
21
+
22
+ # Defines Ruby feature set.
23
+ class Ruby
24
+ # Extra libraries to be loaded from `RbConfig::CONFIG["rubyarchdir"]`.
25
+ #
26
+ # @note This is only required to be called if extra dependencies are necessary.
27
+ # For example, psych.so requires libyaml.so.
28
+ #
29
+ # @note There is stdlib= method also. Yardoc is broken.
30
+ #
31
+ # @return [Array] list of needed libraries.
32
+ attr_reader :stdlib
33
+
34
+ def initialize
35
+ @stdlib = []
36
+ end
37
+
38
+ # @see enabled=
39
+ def enabled?
40
+ @enabled
41
+ end
42
+
43
+ # Enable Ruby feature set.
44
+ #
45
+ # Among others, binds `RbConfig::CONFIG["sitedir"]` so scripts works.
46
+ #
47
+ # @note This does not allow development headers needed for compilation for now.
48
+ # I’ll look at it after I have an use for it.
49
+ def enable
50
+ @enabled = true
51
+ end
52
+
53
+ # Disable Ruby feature set.
54
+ def disable
55
+ @enabled = false
56
+ end
57
+
58
+ # @see #stdlib
59
+ def stdlib= libs
60
+ # Just a little check to have error earlier.
61
+ libs.each do |lib|
62
+ unless File.exist? "#{RbConfig::CONFIG["rubyarchdir"]}/#{lib}.so"
63
+ raise "Library “#{lib}” passed to Bwrap::Config::Ruby.stdlib= does not exist."
64
+ end
65
+ end
66
+
67
+ @stdlib = libs
68
+ end
69
+ end
70
+
71
+ # @return [Bash] Instance of feature class for Bash
72
+ def bash
73
+ @bash ||= Bash.new
74
+ end
75
+
76
+ # @return [Ruby] Instance of feature class for Ruby
77
+ def ruby
78
+ @ruby ||= Ruby.new
79
+ end
80
+ end
81
+ end
data/lib/bwrap/config.rb CHANGED
@@ -3,10 +3,20 @@
3
3
  require "bwrap/output"
4
4
  require "bwrap/version"
5
5
 
6
- # Represents configuration set by user for bwrap execution.
6
+ # Features classes, used through {Config#features}.
7
+ require_relative "config/features"
8
+
9
+ # Represents configuration used to tailor bwrap execution.
10
+ #
11
+ # @developer_note
12
+ # Logger methods (debug, warn and so on) can’t be used here as logger
13
+ # isn’t initialized before this class.
14
+ #
15
+ # For same reason, it’s not advised to execute anything here.
7
16
  #
8
- # Implementation NOTE: logger methods (debug, warn and so on) can’t be used here as logger
9
- # isn’t initialized before this class.
17
+ # Note that all attributes also have writers, even though they are not documented.
18
+ #
19
+ # @todo Add some documentation about syntax where necessary, like for #binaries_from.
10
20
  class Bwrap::Config
11
21
  attr_accessor :hostname
12
22
 
@@ -45,10 +55,11 @@ class Bwrap::Config
45
55
  # @return [Boolean] true if network should be shared from host.
46
56
  attr_accessor :share_net
47
57
 
48
- # TODO: This should cause Bind#full_system_mounts to mount /lib64/ld-linux-x86-64.so.2 and so on,
49
- # probably according executable type of specified command. But that needs some magic.
58
+ # Causes libraries required by the executable given to {Bwrap#run} to be
59
+ # mounted inside sandbox.
50
60
  #
51
- # For now this just mounts all relevant files it can find.
61
+ # Often it is enough to use this flag instead of binding all system libraries
62
+ # using {#libdir_mounts=}
52
63
  #
53
64
  # @return [Boolean] true if Linux library loaders are mounted inside chroot
54
65
  attr_accessor :full_system_mounts
@@ -58,9 +69,20 @@ class Bwrap::Config
58
69
  #
59
70
  # /usr/bin can be mounted using {Config#binaries_from=}.
60
71
  #
72
+ # Often it is enough to use {#full_system_mounts=} instead of binding all
73
+ # system libraries using this flag.
74
+ #
61
75
  # @return [Boolean] true if libdirs are mounted to the chroot
62
76
  attr_accessor :libdir_mounts
63
77
 
78
+ # Set to `true` if command given to {Bwrap::Bwrap#run} is expected to
79
+ # be inside sandbox, and not bound from host.
80
+ #
81
+ # @return [Boolean] `true` if executed command is inside sandbox
82
+ attr_accessor :command_inside_root
83
+
84
+ attr_accessor :extra_executables
85
+
64
86
  # Array of directories to be bind mounted and used to construct PATH environment variable.
65
87
  attr_reader :binaries_from
66
88
 
@@ -72,71 +94,38 @@ class Bwrap::Config
72
94
  # Use given directory as root. End result is similar to classic chroot.
73
95
  attr_reader :root
74
96
 
75
- # `Hash`[`Pathname`] => `Pathname` containing custom read-only binds.
97
+ # @overload ro_binds
98
+ # `Hash`[`Pathname`] => `Pathname` containing custom read-only binds.
99
+ # @overload ro_binds=
100
+ # Set given hash of paths to be bound with --ro-bind.
101
+ #
102
+ # Key is source path, value is destination path.
103
+ #
104
+ # Given source paths must exist.
76
105
  attr_reader :ro_binds
77
106
 
78
- # Path to temporary directory.
107
+ # @overload tmpdir
108
+ # Path to temporary directory.
109
+ #
110
+ # Defaults to Dir.tmpdir.
111
+ # @overload tmpdir=(dir)
112
+ # Sets given directory as temporary directory for certain operations.
79
113
  #
80
- # Defaults to Dir.tmpdir.
114
+ # @note Requires `dir` to be path to existing directory.
115
+ # @raise [RuntimeError] If given directory does not exist
116
+ # @param dir Path to temporary directory
81
117
  attr_reader :tmpdir
82
118
 
83
- # Methods to enable or disable feature sets to control various aspects of sandboxing.
84
- class Features
85
- # Defines Ruby feature set.
86
- class Ruby
87
- # @return [Array] list of needed libraries.
88
- attr_reader :stdlib
89
-
90
- def initialize
91
- @stdlib = []
92
- end
93
-
94
- # @see enabled=
95
- def enabled?
96
- @enabled
97
- end
98
-
99
- # Enable Ruby feature set.
100
- #
101
- # Among others, binds RbConfig::CONFIG["sitedir"] so scripts works.
102
- #
103
- # @note This does not allow development headers needed for compilation for now.
104
- # I’ll look at it after I have an use for it.
105
- def enable
106
- @enabled = true
107
- end
108
-
109
- # Disable Ruby feature set.
110
- def disable
111
- @enabled = false
112
- end
113
-
114
- # Extra libraries to be loaded from `RbConfig::CONFIG["rubyarchdir"]`.
115
- #
116
- # @note This is only required to be called if extra dependencies are necessary.
117
- # For example, psych.so requires libyaml.so.
118
- def stdlib= libs
119
- # Just a little check to have error earlier.
120
- libs.each do |lib|
121
- unless File.exist? "#{RbConfig::CONFIG["rubyarchdir"]}/#{lib}.so"
122
- raise "Library “#{lib}” passed to Bwrap::Config::Ruby.stdlib= does not exist."
123
- end
124
- end
125
-
126
- @stdlib = libs
127
- end
128
- end
129
-
130
- # @return [Ruby] Instance of feature class for Ruby
131
- def ruby
132
- @ruby ||= Ruby.new
133
- end
134
- end
119
+ # Paths to be added to sandbox instance’s PATH environment variable.
120
+ #
121
+ # @see #add_env_path
122
+ attr_reader :env_paths
135
123
 
136
124
  def initialize
137
125
  @binaries_from = []
138
126
  @tmpdir = Dir.tmpdir
139
127
  @audio = []
128
+ @env_paths = []
140
129
  end
141
130
 
142
131
  def binaries_from= array
@@ -178,14 +167,13 @@ class Bwrap::Config
178
167
  @root = directory
179
168
  end
180
169
 
181
- # Set given hash of paths to be bound with --ro-bind.
182
- #
183
- # Key is source path, value is destination path.
184
- #
185
- # Given source paths must exist.
186
170
  def ro_binds= binds
187
171
  @ro_binds = {}
188
172
  binds.each do |source_path, destination_path|
173
+ if destination_path.nil?
174
+ raise "binds should be key-value storage of Strings, for example a Hash."
175
+ end
176
+
189
177
  source_path = Pathname.new source_path
190
178
  unless source_path.exist?
191
179
  raise "Given read only bind does not exist. Please check path “#{source_path}” is correct."
@@ -197,14 +185,17 @@ class Bwrap::Config
197
185
  end
198
186
  end
199
187
 
200
- # Sets given directory as temporary directory for certain operations.
201
- #
202
- # @note Requires `dir` to be path to existing directory.
203
- # @raise [RuntimeError] If given directory does not exist
204
- # @param dir Path to temporary directory
188
+ # See attr_accessor :tmpdir for documentation.
205
189
  def tmpdir= dir
206
190
  raise "Directory to be set as a temporary directory, “#{dir}”, does not exist." unless Dir.exist? dir
207
191
 
208
192
  @tmpdir = dir
209
193
  end
194
+
195
+ # Add a path to sandbox instance’s PATH environment variable.
196
+ #
197
+ # @param path [String] Path to be added added to PATH environment variable
198
+ def add_env_path path
199
+ @env_paths << path
200
+ end
210
201
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bwrap::Execution
4
+ # Unspecified execution related error.
5
+ class CommandError < StandardError
6
+ end
7
+
8
+ # Signifies that command execution has failed.
9
+ class ExecutionFailed < CommandError
10
+ end
11
+
12
+ # Thrown if given command was not found.
13
+ class CommandNotFound < CommandError
14
+ # Command that was looked at.
15
+ attr_reader :command
16
+
17
+ def initialize command:
18
+ @command = command
19
+ msg = "Failed to find #{command} from PATH."
20
+
21
+ super msg
22
+ end
23
+ end
24
+ end
@@ -3,11 +3,14 @@
3
3
  # force_encoding modifies string, so can’t freeze strings.
4
4
 
5
5
  require "bwrap/output"
6
+ require_relative "exceptions"
6
7
  require_relative "execution"
7
8
 
8
9
  # Methods that performs actual execution logic.
9
10
  #
10
- # @api internal
11
+ # @note This is kind of pseudo-internal API. Hopefully there won’t be breaking changes.
12
+ # But please use {Bwrap::Execution} instead of relying functionality of this class,
13
+ # outside of {.prepend_rootcmd}.
11
14
  class Bwrap::Execution::Execute
12
15
  include Bwrap::Output
13
16
 
@@ -1,8 +1,152 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bwrap/version"
4
+ require "bwrap/output"
5
+ require_relative "exceptions"
6
+ require_relative "execute"
7
+ require_relative "path"
4
8
 
5
- # Generic execution functionality.
9
+ # Provides methods to execute commands and handle its output.
10
+ #
11
+ # Output can be controlled by using log levels of Output module.
12
+ #
13
+ # @example Executing a command
14
+ # class MyClass
15
+ # include Bwrap::Execution
16
+ #
17
+ # def my_method
18
+ # execute %w{ ls /dev/null }
6
19
  module Bwrap::Execution
7
- # Nyan.
8
- end
20
+ include Bwrap::Output
21
+ include Bwrap::Execution::Path
22
+
23
+ # Actual implementation of execution command. Can be used when static method is needed.
24
+ #
25
+ # @note When an array is given as a command, empty strings are passed as empty arguments.
26
+ #
27
+ # This means that `[ "foo", "bar" ]` passes one argument to "foo" command, when
28
+ # `[ "foo", "", "bar" ]` passes two arguments.
29
+ #
30
+ # This may or may not be what is assumed, so it can’t be fixed here. It is up to the
31
+ # command to decide how to handle empty arguments.
32
+ #
33
+ # Returns pid of executed command if wait is false.
34
+ # Returns command output if wait is true.
35
+ #
36
+ # fail == If true, an error is raised in case the command returns failure code.
37
+ #
38
+ # @param error if :show, warn()s output of the command is shown if execution failed.
39
+ #
40
+ # @see #execute
41
+ def self.do_execute command,
42
+ fail: true,
43
+ wait: true,
44
+ log: true,
45
+ direct_output: false,
46
+ env: {},
47
+ clear_env: false,
48
+ error: nil,
49
+ log_callback: 2,
50
+ rootcmd: nil
51
+ command = Execute.format_command command, rootcmd: rootcmd
52
+ Execute.handle_logging command, log_callback: log_callback, log: log, dry_run: @dry_run
53
+ return if @dry_run || Bwrap::Execution::Execute.dry_run
54
+
55
+ Execute.open_pipes direct_output
56
+
57
+ # If command is an array, there can’t be arrays inside the array.
58
+ # For convenience, the array is flattened here, so callers can construct commands more easily.
59
+ if command.respond_to? :flatten!
60
+ command.flatten!
61
+ end
62
+
63
+ # If command is string, splat operator (the *) does not do anything. If array, it expand the arguments.
64
+ # This causes spawning work correctly, as that’s how spawn() expects to have the argu
65
+ pid = spawn(env, *command, err: [ :child, :out ], out: Execute.w, unsetenv_others: clear_env)
66
+ output = Execute.finish_execution(log: log, wait: wait, direct_output: direct_output)
67
+ return pid unless wait
68
+
69
+ # This is instant return, but allows us to have $?/$CHILD_STATUS set.
70
+ Process.wait pid
71
+ @last_status = $CHILD_STATUS
72
+
73
+ output = Execute.process_output output: output
74
+ Execute.handle_execution_fail fail: fail, error: error, output: output
75
+ output
76
+ ensure
77
+ Execute.clean_variables
78
+ end
79
+
80
+ # Returns Process::Status instance of last execution.
81
+ #
82
+ # @note This only is able to return the status if wait is true, as otherwise caller is assumed to
83
+ # handle execution flow.
84
+ def self.last_status
85
+ @last_status
86
+ end
87
+
88
+ # Execute a command.
89
+ #
90
+ # This method can be used by including Execution module in a class that should be able to
91
+ # execute commands.
92
+ #
93
+ # @see .do_execute .do_execute for documentation of argument syntax
94
+ private def execute *args
95
+ # Mangle proper location to error message.
96
+ if args.last.is_a? Hash
97
+ args.last[:log_callback] = 3
98
+ else
99
+ args << { log_callback: 3 }
100
+ end
101
+ Bwrap::Execution.do_execute(*args)
102
+ end
103
+
104
+ # Same as ::execute, but uses log: false to avoid unnecessary output when we’re just getting a
105
+ # value for internal needs.
106
+ #
107
+ # Defaults to fail: false, since when one just wants to get the value, there is not that much
108
+ # need to unconditionally die if getting bad exit code.
109
+ private def execvalue *args, fail: false, rootcmd: nil, env: {}
110
+ # This logging handling is a bit of duplication from execute(), but to be extra safe, it is duplicated.
111
+ # The debug message contents will always be evaluated, so can just do it like this.
112
+ log_command = args[0].respond_to?(:join) && args[0].join(" ") || args[0]
113
+ log_command =
114
+ if log_command.frozen?
115
+ log_command.dup.force_encoding("UTF-8")
116
+ else
117
+ log_command.force_encoding("UTF-8")
118
+ end
119
+ if @dry_run
120
+ puts "Would execvalue “#{log_command}” at #{caller_locations(1, 1)[0]}"
121
+ return
122
+ end
123
+ trace "Execvaluing “#{log_command}” at #{caller_locations(1, 1)[0]}"
124
+ execute(*args, fail: fail, log: false, rootcmd: rootcmd, env: env)
125
+ end
126
+
127
+ private def exec_success?
128
+ $CHILD_STATUS.success?
129
+ end
130
+
131
+ private def exec_failure?
132
+ !exec_success?
133
+ end
134
+
135
+ # When running through bundler, don’t use whatever it defines as we’re running inside chroot.
136
+ private def clean_execute
137
+ if (Bundler&.bundler_major_version) >= 2
138
+ Bundler.with_unbundled_env do
139
+ yield 2
140
+ end
141
+ elsif Bundler&.bundler_major_version == 1
142
+ Bundler.with_clean_env do
143
+ yield 1
144
+ end
145
+ else
146
+ yield nil
147
+ end
148
+ rescue NameError
149
+ # If NameError is thrown, no Bundler is available.
150
+ yield nil
151
+ end
152
+ end
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bwrap/version"
4
- require_relative "execution"
4
+
5
+ # Just declaring the module here so full Execution module
6
+ # doesn’t need to be required just to have labels.
7
+ #
8
+ # Most users probably should just require bwrap/execution directly
9
+ # instead of this file, but bwrap/output.rb benefits from this.
10
+ module Bwrap::Execution
11
+ end
5
12
 
6
13
  # Exit codes for exit status handling.
7
14
  #