bwrap 1.0.0.pre.alpha3 → 1.0.0.pre.beta2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,24 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bwrap/execution"
3
4
  require "bwrap/output"
4
5
  require_relative "args"
5
6
 
6
7
  # Environment variable calculation for bwrap.
7
8
  class Bwrap::Args::Environment < Hash
9
+ include Bwrap::Execution
8
10
  include Bwrap::Output
9
11
 
10
12
  # Instance of {Config}.
11
13
  attr_writer :config
12
14
 
13
- # Returns used environment variables.
15
+ def initialize
16
+ super
17
+
18
+ self["PATH"] ||= []
19
+ end
20
+
21
+ # Returns used environment variables wrapped as bwrap arguments.
14
22
  def environment_variables
15
23
  if debug?
16
24
  debug "Passing following environment variables to bwrap:\n" \
17
25
  "#{self}"
18
26
  end
19
27
 
28
+ env_paths
29
+
20
30
  map do |key, value|
31
+ if key == "PATH" and value.respond_to? :join
32
+ value = value.join ":"
33
+ end
34
+
21
35
  [ "--setenv", key, value ]
22
36
  end
23
37
  end
38
+
39
+ # @return [Array] All environment paths added via {Config#add_env_path} and other parsing logic
40
+ def env_paths
41
+ if @config.env_paths.respond_to? :each
42
+ self["PATH"] |= @config.env_paths
43
+ end
44
+
45
+ features_env_paths
46
+
47
+ self["PATH"]
48
+ end
49
+
50
+ # Adds given paths to PATH environment variable defined in the sandbox.
51
+ #
52
+ # @param elements [String, Array] Path(s) to be added added to PATH environment variable
53
+ def add_to_path elements
54
+ if elements.respond_to? :each
55
+ self["PATH"] += elements
56
+ else
57
+ # Expecting elements to be single path element as a string.
58
+ self["PATH"] << elements
59
+ end
60
+ end
61
+
62
+ # Feature specific environment path handling.
63
+ private def features_env_paths
64
+ ruby_env_paths
65
+ end
66
+
67
+ # Ruby feature specific environment path handling.
68
+ private def ruby_env_paths
69
+ return unless @config.features.ruby.enabled?
70
+ return unless @config.features.ruby.gem_env_paths?
71
+
72
+ unless command_available? "gem"
73
+ warn "gem is not installed in the system, so can’t add its bindirs to PATH."
74
+ return
75
+ end
76
+
77
+ gempath = execvalue %w{ gem environment gempath }
78
+ gempath.split(":").each do |path|
79
+ self["PATH"] << "#{path}/bin"
80
+ end
81
+ end
24
82
  end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bwrap/output"
4
+ require_relative "args"
5
+ require_relative "library"
6
+
7
+ # Feature parameter construction.
8
+ #
9
+ # @see Config::Features
10
+ class Bwrap::Args::Features < Hash
11
+ include Bwrap::Output
12
+
13
+ # Implementation for Bash feature set.
14
+ #
15
+ # @api private
16
+ class BashBinds
17
+ # Mounts stuff like /bin/bash.
18
+ def bash_mounts
19
+ mounts = []
20
+
21
+ if File.file? "/bin/bash"
22
+ mounts << "--ro-bind" << "/bin/bash" << "/bin/bash"
23
+ end
24
+ if File.file? "/usr/bin/bash"
25
+ mounts << "--ro-bind" << "/usr/bin/bash" << "/usr/bin/bash"
26
+ end
27
+
28
+ mounts
29
+ end
30
+ end
31
+
32
+ # Implementation for nscd feature set.
33
+ #
34
+ # @api private
35
+ class NscdBinds
36
+ # Custom binds needed by the feature.
37
+ def custom_binds
38
+ mounts = []
39
+
40
+ # TODO: Probably some path checking is needed here. Or somewhere.
41
+ # TODO: Since on many systems /var/run is symlinked to /run, that probably should be handled.
42
+ mounts << "--ro-bind" << "/var/run/nscd" << "/var/run/nscd"
43
+
44
+ mounts
45
+ end
46
+ end
47
+
48
+ # Implementation for Ruby feature set.
49
+ #
50
+ # @api private
51
+ class RubyBinds
52
+ # Returns mounts needed by Ruby feature set.
53
+ attr_reader :mounts
54
+
55
+ # Bind system paths so scripts works inside sandbox.
56
+ def sitedir_mounts
57
+ mounts = []
58
+ mounts << "--ro-bind" << RbConfig::CONFIG["sitedir"] << RbConfig::CONFIG["sitedir"]
59
+ mounts << "--ro-bind" << RbConfig::CONFIG["rubyhdrdir"] << RbConfig::CONFIG["rubyhdrdir"]
60
+ mounts << "--ro-bind" << RbConfig::CONFIG["rubylibdir"] << RbConfig::CONFIG["rubylibdir"]
61
+ mounts << "--ro-bind" << RbConfig::CONFIG["vendordir"] << RbConfig::CONFIG["vendordir"]
62
+
63
+ mounts
64
+ end
65
+
66
+ # Create binds for required system libraries.
67
+ #
68
+ # These are in path like /usr/lib64/ruby/2.5.0/x86_64-linux-gnu/,
69
+ # and as they are mostly shared libraries, they may have some extra
70
+ # dependencies that also need to be bound inside the sandbox.
71
+ def stdlib_mounts stdlib
72
+ library_mounts = []
73
+ library = Bwrap::Args::Library.new
74
+ stdlib.each do |lib|
75
+ path = "#{RbConfig::CONFIG["rubyarchdir"]}/#{lib}.so"
76
+
77
+ library.needed_libraries(path).each do |requisite_library|
78
+ library_mounts << "--ro-bind" << requisite_library << requisite_library
79
+ end
80
+ end
81
+
82
+ library_mounts
83
+ end
84
+ end
85
+
86
+ # `Array` of parameters passed to bwrap.
87
+ attr_writer :args
88
+
89
+ # Instance of {Config}.
90
+ attr_writer :config
91
+
92
+ # Resolves binds required by different features.
93
+ #
94
+ # Currently implemented feature sets:
95
+ # - ruby
96
+ def feature_binds
97
+ bash_binds
98
+ nscd_binds
99
+ ruby_binds
100
+ end
101
+
102
+ private def bash_binds
103
+ return unless @config.features.bash.enabled?
104
+
105
+ binds = BashBinds.new
106
+
107
+ @args.append binds.bash_mounts
108
+ end
109
+
110
+ private def nscd_binds
111
+ return unless @config.features.nscd.enabled?
112
+
113
+ binds = NscdBinds.new
114
+
115
+ @args.append binds.custom_binds
116
+ end
117
+
118
+ # @note This does not allow development headers needed for compilation for now.
119
+ # I’ll look at it after I have an use for it.
120
+ private def ruby_binds
121
+ return unless @config.features.ruby.enabled?
122
+
123
+ binds = RubyBinds.new
124
+
125
+ @args.append binds.sitedir_mounts
126
+ @args.append binds.stdlib_mounts(@config.features.ruby.stdlib)
127
+ end
128
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bwrap/execution"
4
+ require "bwrap/output"
5
+ require_relative "args"
6
+
7
+ # Class to clean up namespace for implementation specific reasons.
8
+ #
9
+ # @api private
10
+ class Bwrap::Args::Library
11
+ include Bwrap::Execution
12
+ include Bwrap::Output
13
+
14
+ # NOTE: This caching can be made more efficient, but need to look at it later, what to do about it.
15
+ @@needed_libraries_cache ||= []
16
+
17
+ # Empties {@@needed_libraries_cache}.
18
+ def self.clear_needed_libraries_cache
19
+ @@needed_libraries_cache.clear
20
+ end
21
+
22
+ # Otherwise similar to {#needed_libraries}, but checks used libc to handle musl executables.
23
+ #
24
+ # @param executable [String] Path to the executable to find dependencies for
25
+ def libraries_needed_by executable
26
+ # %i == interpreter, the library used to load the executable by kernel.
27
+ # %F == Path to given file.
28
+ output_format = "%i::SEPARATOR::%F"
29
+ scanelf_command = %W{ scanelf --nobanner --quiet --format #{output_format} }
30
+ scanelf_command << executable
31
+
32
+ data = execvalue scanelf_command
33
+ data = data.strip
34
+ interpreter, _executable_path = data.split "::SEPARATOR::"
35
+ interpreter = Pathname.new interpreter
36
+
37
+ if interpreter.basename.to_s[0..6] == "ld-musl"
38
+ musl_needed_libraries executable
39
+ else
40
+ # For glibc, scanelf can return full paths for us most of time.
41
+ needed_libraries executable
42
+ end
43
+ end
44
+
45
+ # @param binary_paths [String, Array] one or more paths to be resolved
46
+ #
47
+ # @todo Maybe caching should be done here too?
48
+ def musl_needed_libraries binary_paths
49
+ trace "Finding musl libraries #{binary_paths} requires"
50
+ @needed_libraries = []
51
+
52
+ if binary_paths.is_a? String
53
+ binary_paths = [ binary_paths ]
54
+ end
55
+
56
+ binary_paths.each do |binary_path|
57
+ output = execvalue %W{ ldd #{binary_path} }
58
+ lines = output.split "\n"
59
+ _interpreter_line = lines.shift
60
+
61
+ lines.each do |line|
62
+ parse_ldd_line line
63
+ end
64
+ end
65
+
66
+ @needed_libraries
67
+ end
68
+
69
+ # Used by {Bwrap::Args::Bind#libs_command_requires}.
70
+ #
71
+ # @param binary_paths [String, Array] one or more paths to be resolved
72
+ def needed_libraries binary_paths
73
+ trace "Finding libraries #{binary_paths} requires"
74
+ @needed_libraries = []
75
+
76
+ # %i == interpreter, the library used to load the executable by kernel.
77
+ output_format = "%F::SEPARATOR::%n"
78
+ scanelf_command = %W{ scanelf --nobanner --quiet --format #{output_format} --ldcache --needed }
79
+
80
+ if binary_paths.is_a? String
81
+ binary_paths = [ binary_paths ]
82
+ end
83
+
84
+ # Check if the exe is already resolved.
85
+ binary_paths.delete_if do |binary_path|
86
+ @@needed_libraries_cache.include? binary_path
87
+ end
88
+
89
+ return [] if binary_paths.empty?
90
+
91
+ data = execvalue(scanelf_command + binary_paths)
92
+ trace "scanelf found following libraries: #{data}"
93
+
94
+ lines = data.split "\n"
95
+ lines.each do |line|
96
+ parse_scanelf_line line
97
+ end
98
+
99
+ @needed_libraries
100
+ end
101
+
102
+ # Used by {#needed_libraries}.
103
+ private def parse_scanelf_line line
104
+ binary_path, libraries_line = line.split "::SEPARATOR::"
105
+ libraries = libraries_line.split ","
106
+
107
+ (@needed_libraries & libraries).each do |library|
108
+ verb "Binding #{library} as dependency of #{binary_path}"
109
+ end
110
+
111
+ @needed_libraries |= libraries
112
+
113
+ # Also check if requisite libraries needs some libraries.
114
+ inner = Bwrap::Args::Library.new
115
+ @needed_libraries |= inner.needed_libraries libraries
116
+
117
+ @@needed_libraries_cache |= libraries
118
+ end
119
+
120
+ # Used by {#musl_needed_libraries}.
121
+ private def parse_ldd_line line
122
+ line = line.strip
123
+ _library_name, library_data = line.split " => "
124
+
125
+ matches = library_data.match(/(.*) \(0x[0-9a-f]+\)/)
126
+ library_path = matches[1]
127
+
128
+ unless @needed_libraries.include? library_path
129
+ @needed_libraries << library_path
130
+ end
131
+
132
+ # Also check if requisite libraries needs some libraries.
133
+ inner = Bwrap::Args::Library.new
134
+ @needed_libraries |= inner.musl_needed_libraries library_path
135
+ end
136
+ end
137
+ # class Library ended
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "securerandom"
4
+ require "tempfile"
4
5
 
5
6
  require "bwrap/output"
6
7
  require_relative "args"
@@ -24,7 +25,7 @@ class Bwrap::Args::MachineId
24
25
  # Returning [] means that execute() will ignore this fully.
25
26
  # Nil would be converted to empty string, causing spawn() to pass it as argument, causing
26
27
  # bwrap to misbehave.
27
- return [] unless @config.machine_id
28
+ return unless @config&.machine_id
28
29
 
29
30
  machine_id = @config.machine_id
30
31
 
@@ -51,10 +52,10 @@ class Bwrap::Args::MachineId
51
52
  debug "Using random machine id as /etc/machine-id"
52
53
 
53
54
  @machine_id_file = Tempfile.new "bwrap-random_machine_id-", @config.tmpdir
54
- @machine_id_file.write SecureRandom.uuid.gsub("-", "")
55
+ @machine_id_file.write SecureRandom.uuid.tr("-", "")
55
56
  @machine_id_file.flush
56
57
 
57
- %W{ --ro-bind-data #{machine_id_file.fileno} /etc/machine-id }
58
+ %W{ --ro-bind-data #{@machine_id_file.fileno} /etc/machine-id }
58
59
  end
59
60
 
60
61
  # Uses `10000000000000000000000000000000` as machine id.
@@ -79,6 +80,8 @@ class Bwrap::Args::MachineId
79
80
  end
80
81
 
81
82
  # Uses file inside sandbox directory as machine id.
83
+ #
84
+ # TODO: I kind of want to deprecate this one. It may make sense, but eh... Let’s see.
82
85
  private def machine_id_inside_sandbox_dir sandbox_directory
83
86
  machine_id_file = "#{sandbox_directory}/machine-id"
84
87
 
@@ -4,21 +4,30 @@ require_relative "args"
4
4
 
5
5
  # Bind arguments for bwrap.
6
6
  module Bwrap::Args::Mount
7
+ # Arguments for readwrite-binding {Config#root} as /.
8
+ private def root_mount
9
+ return unless @config.root
10
+
11
+ debug "Binding #{@config.root} as /"
12
+ @args.append %W{ --bind #{@config.root} / }
13
+ @args.append %w{ --chdir / }
14
+ end
15
+
7
16
  # Arguments for mounting devtmpfs to /dev.
8
17
  private def dev_mount
9
18
  debug "Mounting new devtmpfs to /dev"
10
- %w{ --dev /dev }
19
+ @args.append %w{ --dev /dev }
11
20
  end
12
21
 
13
22
  # Arguments for mounting procfs to /proc.
14
23
  private def proc_mount
15
24
  debug "Mounting new procfs to /proc"
16
- %w{ --proc /proc }
25
+ @args.append %w{ --proc /proc }
17
26
  end
18
27
 
19
28
  # Arguments for mounting tmpfs to /tmp.
20
29
  private def tmp_as_tmpfs
21
30
  debug "Mounting tmpfs to /tmp"
22
- %w{ --tmpfs /tmp }
31
+ @args.append %w{ --tmpfs /tmp }
23
32
  end
24
33
  end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ #require "deep-cover" if ENV["DEEP_COVER"]
4
+
5
+ require_relative "bwrap_module"
6
+ require "bwrap/version"
7
+ require "bwrap/args/construct"
8
+ require "bwrap/config"
9
+ require "bwrap/execution"
10
+
11
+ # Executes given command under {https://github.com/containers/bubblewrap bwrap}
12
+ # using given configuration.
13
+ #
14
+ # @see ::Bwrap Bwrap module for usage example
15
+ class Bwrap::Bwrap
16
+ include Bwrap::Execution
17
+
18
+ # @param config [Bwrap::Config] Configuration used to tailor bwrap
19
+ def initialize config
20
+ # TODO: Ensure that costruct.rb and utilities it uses does not enforce Config to be valid object.
21
+ # Create a test for that also. Well, as long as that makes sense. If it doesn’t work, validate
22
+ # Config object to be valid here.
23
+ @config = config
24
+ end
25
+
26
+ # Parses command line arguments given to caller script.
27
+ #
28
+ # @note This method automatically sets output levels according parsed
29
+ # options. It is also possible to set the levels manually using
30
+ # {Bwrap::Output.handle_output_options}, for example with flags
31
+ # parsed with optparse’s `OptionParser`, found in Ruby’s standard
32
+ # library.
33
+ #
34
+ # @warning Requires optimist gem to be installed, which is not a dependency of this gem.
35
+ def parse_command_line_arguments
36
+ options = parse_options
37
+
38
+ Bwrap::Output.handle_output_options options
39
+ end
40
+
41
+ # Binds and executes given command available on running system inside bwrap.
42
+ #
43
+ # If {Config#root} has been set, given executable is scanned for necessary
44
+ # libraries and they are bound inside sandbox. This often reduces amount of
45
+ # global binds, but some miscellaneous things may need custom handling.
46
+ #
47
+ # @see Config#features about binding bigger feature sets to sandbox.
48
+ #
49
+ # Executed command is constructed using configuration passed to constructor.
50
+ # After execution has completed, bwrap will also shut down.
51
+ #
52
+ # @param command [String, Array] Command to be executed inside bwrap along with necessary arguments
53
+ # @see #run_inside_root to execute a command that already is inside sandbox.
54
+ def run command
55
+ construct = Bwrap::Args::Construct.new
56
+ construct.command = command
57
+ construct.config = @config
58
+ bwrap_args = construct.construct_bwrap_args
59
+
60
+ exec_command = [ "bwrap" ]
61
+ exec_command += bwrap_args
62
+ exec_command.append command
63
+ exec_command += @cli_args if @cli_args
64
+
65
+ execute exec_command
66
+
67
+ construct.cleanup
68
+ end
69
+
70
+ # Convenience method to executes a command that is inside bwrap.
71
+ #
72
+ # Given command is expected to already be inside {Config#root}.
73
+ #
74
+ # Calling this method is equivalent to setting {Config#command_inside_root} to `true`.
75
+ #
76
+ # @note This may have a bit unintuitive usage, as most things are checked anyway, so this is not
77
+ # akin to running something inside a chroot, but rather for convenience.
78
+ #
79
+ # @param command [String, Array] Command to be executed inside bwrap along with necessary arguments
80
+ # @see #run to execute a command that needs to be bound to the sandbox.
81
+ def run_inside_root command
82
+ if @config
83
+ config = @config.dup
84
+ config.command_inside_root = true
85
+ else
86
+ config = nil
87
+ end
88
+
89
+ construct = Bwrap::Args::Construct.new
90
+ construct.command = command
91
+ construct.config = config
92
+ bwrap_args = construct.construct_bwrap_args
93
+
94
+ exec_command = [ "bwrap" ]
95
+ exec_command += bwrap_args
96
+ exec_command.append command
97
+ exec_command += @cli_args if @cli_args
98
+
99
+ execute exec_command
100
+
101
+ construct.cleanup
102
+ end
103
+
104
+ # Parses global bwrap flags using Optimist.
105
+ #
106
+ # Sets instance variable `@cli_args`.
107
+ #
108
+ # @warning Requires optimist gem to be installed, which is not a dependency of this gem.
109
+ #
110
+ # @api private
111
+ # @return [Hash] options parsed by `Optimist.options`
112
+ private def parse_options
113
+ begin
114
+ require "optimist"
115
+ rescue LoadError => e
116
+ puts "Failed to load optimist gem. In order to use Bwrap::Bwrap#parse_command_line_arguments, " \
117
+ "ensure optimist gem is present for example in your Gemfile."
118
+ puts
119
+ raise e
120
+ end
121
+
122
+ options = Optimist.options do
123
+ version ::Bwrap::VERSION
124
+
125
+ banner "Usage:"
126
+ banner " #{$PROGRAM_NAME} [global options]\n \n"
127
+ banner "Global options:"
128
+ opt :verbose,
129
+ "Show verbose output",
130
+ short: "v"
131
+ opt :debug,
132
+ "Show debug output (useful when debugging a problem)",
133
+ short: "d"
134
+ opt :trace,
135
+ "Show trace output (noisiest, probably not useful for most of time)",
136
+ short: :none
137
+ opt :version,
138
+ "Print version and exit",
139
+ short: "V"
140
+ opt :help,
141
+ "Show help message",
142
+ short: "h"
143
+
144
+ educate_on_error
145
+ end
146
+
147
+ @cli_args = ARGV.dup
148
+
149
+ options
150
+ end
151
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ruby-bwrap provides easy-to-use interface to run complex programs in sandboxes created with
4
+ # {https://github.com/containers/bubblewrap bubblewrap}.
5
+ #
6
+ # To run a program inside bubblewrap, a wrapper executable can be created. For example:
7
+ #
8
+ # require "bwrap"
9
+ #
10
+ # config = Bwrap::Config.new
11
+ # config.user = "dummy_user"
12
+ # config.full_system_mounts = true
13
+ # config.binaries_from = %w{
14
+ # /bin
15
+ # /usr/bin
16
+ # }
17
+ #
18
+ # bwrap = Bwrap::Bwrap.new config
19
+ # bwrap.parse_command_line_arguments
20
+ # bwrap.run "/bin/true"
21
+ #
22
+ # There also are few generic utilities, {Bwrap::Output} for handling output of scripts and
23
+ # {Bwrap::Execution} to run executables.
24
+ module Bwrap
25
+ # Empty module.
26
+ end