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.
@@ -0,0 +1,116 @@
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
+ # @abstract
7
+ #
8
+ # Base of all features.
9
+ class Base
10
+ # @param features [Bwrap::Config::Features] Instance of features object in {Config}
11
+ def initialize features
12
+ @features = features
13
+ end
14
+
15
+ # Checks if the feature has been enabled.
16
+ #
17
+ # @return [Boolean] whether feature is enabled
18
+ def enabled?
19
+ @enabled
20
+ end
21
+
22
+ # Enable the feature.
23
+ def enable
24
+ @enabled = true
25
+ end
26
+
27
+ # Disable the feature.
28
+ def disable
29
+ @enabled = false
30
+ end
31
+ end
32
+
33
+ # Defines Bash feature set.
34
+ class Bash < Base
35
+ # Nya.
36
+ end
37
+
38
+ # Defines Nscd feature set.
39
+ #
40
+ # nscd is short of name service cache daemon. It may make sense to
41
+ # have this class under another name, but I don’t know how nscd specific
42
+ # this feature can be, so this name it is for now.
43
+ class Nscd < Base
44
+ # Nya.
45
+ end
46
+
47
+ # Defines Ruby feature set.
48
+ #
49
+ # Implies {Nscd} feature.
50
+ class Ruby < Base
51
+ # Extra libraries to be loaded from `RbConfig::CONFIG["rubyarchdir"]`.
52
+ #
53
+ # @note This is only required to be called if extra dependencies are necessary.
54
+ # For example, psych.so requires libyaml.so.
55
+ #
56
+ # @return [Array] list of needed libraries.
57
+ #
58
+ # @overload stdlib
59
+ # @overload stdlib=(libs)
60
+ attr_reader :stdlib
61
+
62
+ def initialize features
63
+ super features
64
+
65
+ @gem_env_paths = true
66
+ @stdlib = []
67
+ end
68
+
69
+ # @return true if bindirs from “gem environment” should be added to sandbox.
70
+ def gem_env_paths?
71
+ @gem_env_paths
72
+ end
73
+
74
+ # Enable Ruby feature set.
75
+ #
76
+ # Among others, binds `RbConfig::CONFIG["sitedir"]` so scripts works.
77
+ #
78
+ # @note This does not allow development headers needed for compilation for now.
79
+ # I’ll look at it after I have an use for it.
80
+ #
81
+ # @note Also enables {Nscd} feature.
82
+ def enable
83
+ super
84
+
85
+ @features.nscd.enable
86
+ end
87
+
88
+ # @see #stdlib
89
+ def stdlib= libs
90
+ # Just a little check to have error earlier.
91
+ libs.each do |lib|
92
+ unless File.exist? "#{RbConfig::CONFIG["rubyarchdir"]}/#{lib}.so"
93
+ raise "Library “#{lib}” passed to Bwrap::Config::Ruby.stdlib= does not exist."
94
+ end
95
+ end
96
+
97
+ @stdlib = libs
98
+ end
99
+ end
100
+
101
+ # @return [Bash] Instance of feature class for Bash
102
+ def bash
103
+ @bash ||= Bash.new self
104
+ end
105
+
106
+ # @return [Nscd] Instance of feature class for nscd
107
+ def nscd
108
+ @nscd ||= Nscd.new self
109
+ end
110
+
111
+ # @return [Ruby] Instance of feature class for Ruby
112
+ def ruby
113
+ @ruby ||= Ruby.new self
114
+ end
115
+ end
116
+ end
data/lib/bwrap/config.rb CHANGED
@@ -3,13 +3,60 @@
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
21
+ # Array of audio schemes usable inside chroot.
22
+ #
23
+ # Currently supports:
24
+ # - :pulseaudio
25
+ #
26
+ attr_accessor :audio
27
+
28
+ # Set to `true` if command given to {Bwrap::Bwrap#run} is expected to
29
+ # be inside sandbox, and not bound from host.
30
+ #
31
+ # @return [Boolean] `true` if executed command is inside sandbox
32
+ attr_accessor :command_inside_root
33
+
34
+ attr_accessor :extra_executables
35
+
36
+ # TODO: IIRC this doesn’t match the reality any more. So write correct documentation.
37
+ #
38
+ # Causes libraries required by the executable given to {Bwrap#run} to be
39
+ # mounted inside sandbox.
40
+ #
41
+ # Often it is enough to use this flag instead of binding all system libraries
42
+ # using {#libdir_mounts=}
43
+ #
44
+ # @return [Boolean] true if Linux library loaders are mounted inside chroot
45
+ attr_accessor :full_system_mounts
46
+
11
47
  attr_accessor :hostname
12
48
 
49
+ # Set to true if basic system directories, like /usr/lib and /usr/lib64,
50
+ # should be bound inside chroot.
51
+ #
52
+ # /usr/bin can be mounted using {Config#binaries_from=}.
53
+ #
54
+ # Often it is enough to use {#full_system_mounts=} instead of binding all
55
+ # system libraries using this flag.
56
+ #
57
+ # @return [Boolean] true if libdirs are mounted to the chroot
58
+ attr_accessor :libdir_mounts
59
+
13
60
  # What should be used as /etc/machine_id file.
14
61
  #
15
62
  # If not specified, no /etc/machine_id handling is done.
@@ -24,6 +71,12 @@ class Bwrap::Config
24
71
  # Given file as bound as /etc/machine_id.
25
72
  attr_accessor :machine_id
26
73
 
74
+ # @return [Boolean] true if network should be shared from host.
75
+ attr_accessor :share_net
76
+
77
+ # Name of the user inside chroot.
78
+ #
79
+ # This is optional and defaults to no user.
27
80
  attr_accessor :user
28
81
 
29
82
  # Set to true to indicate we’re running a X.org application, meaning we need to do some extra holes,
@@ -32,21 +85,59 @@ class Bwrap::Config
32
85
  # @return [Boolean] Whether Xorg specific binds are used.
33
86
  attr_accessor :xorg_application
34
87
 
35
- # Array of directories to be bind mounted and used to construct PATH environment variable.
88
+ # Array of directories to be bind mounted in sandbox.
89
+ #
90
+ # Given paths are also added to PATH environment variable inside sandbox.
91
+ #
92
+ # @hint At least on SUSE, many executables are symlinks to /etc/alternatives/*,
93
+ # which in turn symlinks to versioned executable under the same bindir.
94
+ # To use these executables, /etc/alternatives should also be bound:
95
+ #
96
+ # config.ro_binds["/etc/alternatives"] = "/etc/alternatives"
97
+ #
98
+ # @return [Array] Paths to directories where binaries are looked from.
36
99
  attr_reader :binaries_from
37
100
 
101
+ # Paths to be added to sandbox instance’s PATH environment variable.
102
+ #
103
+ # @see #add_env_path
104
+ attr_reader :env_paths
105
+
106
+ # TODO: Document this.
107
+ # TODO: I wonder if this should just be removed. I don’t know, this is a bit ...
108
+ # Well, I can see it can have some merit, but very hard to say.
38
109
  attr_reader :sandbox_directory
39
110
 
40
- # `Hash`[`Pathname`] => `Pathname` containing custom read-only binds.
111
+ # Use given directory as root. End result is similar to classic chroot.
112
+ attr_reader :root
113
+
114
+ # @overload ro_binds
115
+ # `Hash`[`Pathname`] => `Pathname` containing custom read-only binds.
116
+ # @overload ro_binds=
117
+ # Set given hash of paths to be bound with --ro-bind.
118
+ #
119
+ # Key is source path, value is destination path.
120
+ #
121
+ # Given source paths must exist.
41
122
  attr_reader :ro_binds
42
123
 
43
- # Path to temporary directory.
124
+ # @overload tmpdir
125
+ # Path to temporary directory.
126
+ #
127
+ # Defaults to Dir.tmpdir.
128
+ # @overload tmpdir=(dir)
129
+ # Sets given directory as temporary directory for certain operations.
44
130
  #
45
- # Defaults to Dir.tmpdir.
131
+ # @note Requires `dir` to be path to existing directory.
132
+ # @raise [RuntimeError] If given directory does not exist
133
+ # @param dir Path to temporary directory
46
134
  attr_reader :tmpdir
47
135
 
48
136
  def initialize
137
+ @audio = []
49
138
  @binaries_from = []
139
+ @env_paths = []
140
+ @ro_binds = {}
50
141
  @tmpdir = Dir.tmpdir
51
142
  end
52
143
 
@@ -61,6 +152,17 @@ class Bwrap::Config
61
152
  end
62
153
  end
63
154
 
155
+ # Enable or disable feature sets to control various aspects of sandboxing.
156
+ #
157
+ # @example To enable Ruby feature set
158
+ # @config.features.ruby = true
159
+ #
160
+ # @see {Features} List of available features
161
+ # @return [Features] Object used to toggle features
162
+ def features
163
+ @features ||= ::Bwrap::Config::Features.new
164
+ end
165
+
64
166
  def sandbox_directory= directory
65
167
  unless Dir.exist? directory
66
168
  raise "Given sandbox directory #{directory} does not exist. Please create it beforehand and setup to your needs."
@@ -69,14 +171,22 @@ class Bwrap::Config
69
171
  @sandbox_directory = directory
70
172
  end
71
173
 
72
- # Set given hash of paths to be bound with --ro-bind.
73
- #
74
- # Key is source path, value is destination path.
75
- #
76
- # Given source paths must exist.
174
+ # Directory used as writable root, akin to classic chroot.
175
+ def root= directory
176
+ unless Dir.exist? directory
177
+ raise "Given root directory #{directory} does not exist. Please create it beforehand and set up to your needs."
178
+ end
179
+
180
+ @root = directory
181
+ end
182
+
77
183
  def ro_binds= binds
78
184
  @ro_binds = {}
79
185
  binds.each do |source_path, destination_path|
186
+ if destination_path.nil?
187
+ raise "binds should be key-value storage of Strings, for example a Hash."
188
+ end
189
+
80
190
  source_path = Pathname.new source_path
81
191
  unless source_path.exist?
82
192
  raise "Given read only bind does not exist. Please check path “#{source_path}” is correct."
@@ -88,14 +198,17 @@ class Bwrap::Config
88
198
  end
89
199
  end
90
200
 
91
- # Sets given directory as temporary directory for certain operations.
92
- #
93
- # @note Requires `dir` to be path to existing directory.
94
- # @raise [RuntimeError] If given directory does not exist
95
- # @param dir Path to temporary directory
201
+ # See attr_accessor :tmpdir for documentation.
96
202
  def tmpdir= dir
97
203
  raise "Directory to be set as a temporary directory, “#{dir}”, does not exist." unless Dir.exist? dir
98
204
 
99
205
  @tmpdir = dir
100
206
  end
207
+
208
+ # Add a path to sandbox instance’s PATH environment variable.
209
+ #
210
+ # @param path [String] Path to be added added to PATH environment variable
211
+ def add_env_path path
212
+ @env_paths << path
213
+ end
101
214
  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
 
@@ -95,7 +98,7 @@ class Bwrap::Execution::Execute
95
98
  # Stub to instruct implementation in subclass.
96
99
  def self.prepend_rootcmd command, rootcmd:
97
100
  raise NotImplementedError, "If rootcmd execution is necessary, monkey patch Bwrap::Execution::Execute " \
98
- "to add “self.prepend_rootcmd(command, rootcmd:)” method."
101
+ "to add “self.prepend_rootcmd(command, rootcmd:)” method."
99
102
  end
100
103
 
101
104
  # Used by `#handle_logging`.
@@ -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, **kwargs
95
+ # Mangle proper location to error message.
96
+ if kwargs.is_a? Hash
97
+ kwargs[:log_callback] = 3
98
+ else
99
+ kwargs = { log_callback: 3 }
100
+ end
101
+ Bwrap::Execution.do_execute(*args, **kwargs)
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, log: false, **kwargs
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: log, **kwargs)
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
  #
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bwrap/output"
4
+ require_relative "exceptions"
5
+
6
+ # Path checking methods.
7
+ module Bwrap::Execution::Path
8
+ # Utilities to handle environment path operations.
9
+ #
10
+ # @api private
11
+ class Environment
12
+ # Loop through each path in global PATH environment variable to
13
+ # perform an operation in each path, for example to resolve
14
+ # absolute path to a command.
15
+ #
16
+ # NOTE: This has nothing to do with {Bwrap::Config#env_paths}, as the
17
+ # env paths looped are what the system has defined, in ENV["PATH"].
18
+ #
19
+ # Should be cross-platform.
20
+ #
21
+ # @yield Command appended to each path in PATH environment variable
22
+ # @yieldparam path [String] Full path to executable
23
+ def self.each_env_path command, env_path_var: ENV["PATH"]
24
+ exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [ "" ]
25
+
26
+ env_path_var.split(File::PATH_SEPARATOR).each do |env_path|
27
+ exts.each do |ext|
28
+ exe = File.join(env_path, "#{command}#{ext}")
29
+ yield exe
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ # Check if requested program can be found.
36
+ #
37
+ # Should work cross-platform and in restricted environments pretty well.
38
+ #
39
+ # @param command [String] executable to be resolved
40
+ # @param env_path_var [String] PATH environment variable as string.
41
+ # Defaults to `ENV["PATH"]`
42
+ private def command_available? command, env_path_var: ENV["PATH"]
43
+ # Special handling for absolute paths.
44
+ path = Pathname.new command
45
+ if path.absolute?
46
+ if path.executable? && !path.directory?
47
+ return true
48
+ end
49
+
50
+ return false
51
+ end
52
+
53
+ Bwrap::Execution::Path::Environment.each_env_path command, env_path_var: env_path_var do |exe|
54
+ return true if File.executable?(exe) && !File.directory?(exe)
55
+ end
56
+
57
+ false
58
+ end
59
+
60
+ # Returns path to given executable.
61
+ #
62
+ # @param (see #command_available?)
63
+ private def which command, fail: true, env_path_var: ENV["PATH"]
64
+ # Special handling for absolute paths.
65
+ path = Pathname.new command
66
+ if path.absolute?
67
+ if path.executable?
68
+ return command
69
+ end
70
+
71
+ raise Bwrap::Execution::CommandNotFound.new command: command if fail
72
+
73
+ return nil
74
+ end
75
+
76
+ Bwrap::Execution::Path::Environment.each_env_path command, env_path_var: env_path_var do |exe|
77
+ return exe if File.executable?(exe) && !File.directory?(exe)
78
+ end
79
+
80
+ return nil unless fail
81
+
82
+ raise Bwrap::Execution::CommandNotFound.new command: command
83
+ end
84
+ end