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

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.
@@ -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