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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  #