bwrap 1.0.0.pre.alpha2 → 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,103 @@
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 Ruby feature set.
33
+ #
34
+ # @api private
35
+ class RubyBinds
36
+ # Returns mounts needed by Ruby feature set.
37
+ attr_reader :mounts
38
+
39
+ # Bind system paths so scripts works inside sandbox.
40
+ def sitedir_mounts
41
+ mounts = []
42
+ mounts << "--ro-bind" << RbConfig::CONFIG["sitedir"] << RbConfig::CONFIG["sitedir"]
43
+ mounts << "--ro-bind" << RbConfig::CONFIG["rubyhdrdir"] << RbConfig::CONFIG["rubyhdrdir"]
44
+ mounts << "--ro-bind" << RbConfig::CONFIG["rubylibdir"] << RbConfig::CONFIG["rubylibdir"]
45
+ mounts << "--ro-bind" << RbConfig::CONFIG["vendordir"] << RbConfig::CONFIG["vendordir"]
46
+
47
+ mounts
48
+ end
49
+
50
+ # Create binds for required system libraries.
51
+ #
52
+ # These are in path like /usr/lib64/ruby/2.5.0/x86_64-linux-gnu/,
53
+ # and as they are mostly shared libraries, they may have some extra
54
+ # dependencies that also need to be bound inside the sandbox.
55
+ def stdlib_mounts stdlib
56
+ library_mounts = []
57
+ library = Bwrap::Args::Library.new
58
+ stdlib.each do |lib|
59
+ path = "#{RbConfig::CONFIG["rubyarchdir"]}/#{lib}.so"
60
+
61
+ library.needed_libraries(path).each do |requisite_library|
62
+ library_mounts << "--ro-bind" << requisite_library << requisite_library
63
+ end
64
+ end
65
+
66
+ library_mounts
67
+ end
68
+ end
69
+
70
+ # `Array` of parameters passed to bwrap.
71
+ attr_writer :args
72
+
73
+ # Instance of {Config}.
74
+ attr_writer :config
75
+
76
+ # Resolves binds required by different features.
77
+ #
78
+ # Currently implemented feature sets:
79
+ # - ruby
80
+ def feature_binds
81
+ bash_binds
82
+ ruby_binds
83
+ end
84
+
85
+ private def bash_binds
86
+ return unless @config.features.bash.enabled?
87
+
88
+ binds = BashBinds.new
89
+
90
+ @args.append binds.bash_mounts
91
+ end
92
+
93
+ # @note This does not allow development headers needed for compilation for now.
94
+ # I’ll look at it after I have an use for it.
95
+ private def ruby_binds
96
+ return unless @config.features.ruby.enabled?
97
+
98
+ binds = RubyBinds.new
99
+
100
+ @args.append binds.sitedir_mounts
101
+ @args.append binds.stdlib_mounts(@config.features.ruby.stdlib)
102
+ end
103
+ 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,7 +52,7 @@ 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.delete("-", "")
55
56
  @machine_id_file.flush
56
57
 
57
58
  %W{ --ro-bind-data #{machine_id_file.fileno} /etc/machine-id }
@@ -4,21 +4,29 @@ 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
+ end
14
+
7
15
  # Arguments for mounting devtmpfs to /dev.
8
16
  private def dev_mount
9
17
  debug "Mounting new devtmpfs to /dev"
10
- %w{ --dev /dev }
18
+ @args.append %w{ --dev /dev }
11
19
  end
12
20
 
13
21
  # Arguments for mounting procfs to /proc.
14
22
  private def proc_mount
15
23
  debug "Mounting new procfs to /proc"
16
- %w{ --proc /proc }
24
+ @args.append %w{ --proc /proc }
17
25
  end
18
26
 
19
27
  # Arguments for mounting tmpfs to /tmp.
20
28
  private def tmp_as_tmpfs
21
29
  debug "Mounting tmpfs to /tmp"
22
- %w{ --tmpfs /tmp }
30
+ @args.append %w{ --tmpfs /tmp }
23
31
  end
24
32
  end
@@ -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