bwrap 1.1.1 → 1.3.1

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.
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "bwrap/output"
4
4
  require_relative "args"
5
- require_relative "library"
6
5
 
7
6
  # Feature parameter construction.
8
7
  #
@@ -68,7 +67,7 @@ class Bwrap::Args::Features < Hash
68
67
  end
69
68
 
70
69
  private def bash_binds
71
- return unless @config.features.bash.enabled?
70
+ return unless @config and @config.features.bash.enabled?
72
71
 
73
72
  binds = BashBinds.new
74
73
 
@@ -76,7 +75,7 @@ class Bwrap::Args::Features < Hash
76
75
  end
77
76
 
78
77
  private def nscd_binds
79
- return unless @config.features.nscd.enabled?
78
+ return unless @config and @config.features.nscd.enabled?
80
79
 
81
80
  binds = NscdBinds.new
82
81
 
@@ -86,7 +85,7 @@ class Bwrap::Args::Features < Hash
86
85
  # @note This does not allow development headers needed for compilation for now.
87
86
  # I’ll look at it after I have an use for it.
88
87
  private def ruby_binds
89
- return unless @config.features.ruby.enabled?
88
+ return unless @config and @config.features.ruby.enabled?
90
89
 
91
90
  binds = RubyBinds.new @config
92
91
 
@@ -6,19 +6,13 @@ require_relative "args"
6
6
  module Bwrap::Args::Mount
7
7
  # Arguments for readwrite-binding {Config#root} as /.
8
8
  private def root_mount
9
- return unless @config.root
9
+ return unless @config&.root
10
10
 
11
11
  debug "Binding #{@config.root} as /"
12
12
  @args.add :root_mount, "--bind", @config.root, "/"
13
13
  @args.add :root_mount, "--chdir", "/"
14
14
  end
15
15
 
16
- # Arguments for mounting devtmpfs to /dev.
17
- private def dev_mount
18
- debug "Mounting new devtmpfs to /dev"
19
- @args.add :dev_mounts, "--dev", "/dev"
20
- end
21
-
22
16
  # Arguments for mounting procfs to /proc.
23
17
  private def proc_mount
24
18
  debug "Mounting new procfs to /proc"
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bwrap/output"
4
+ require_relative "args"
5
+
6
+ # Namespace related arguments.
7
+ #
8
+ # Mostly for handling --unshare-*
9
+ class Bwrap::Args::Namespace
10
+ include Bwrap::Output
11
+
12
+ # Instance of {Config}.
13
+ attr_writer :config
14
+
15
+ # @param args [Bwrap::Args::Args] Arguments to be passed to bwrap.
16
+ def initialize args
17
+ @args = args
18
+ end
19
+
20
+ def shares
21
+ return unless @config&.unshare_all
22
+
23
+ @args.add :unshare_all, "--unshare-all" # Practically means that there would be nothing in the sandbox by default.
24
+ end
25
+ end
@@ -17,14 +17,19 @@ class Bwrap::Args::Network
17
17
 
18
18
  # Arguments to set hostname to whatever is configured.
19
19
  def hostname
20
- return unless @config.hostname
20
+ return unless @config&.hostname
21
21
 
22
22
  debug "Setting hostname to #{@config.hostname}"
23
23
  @args.add :hostname, %W{ --hostname #{@config.hostname} }
24
24
  end
25
25
 
26
26
  # Arguments to read-only bind /etc/resolv.conf.
27
+ #
28
+ # TODO: Probably it should be checked if target will have the symlink present before
29
+ # doing this automatically. For that reason, now this will need a flag.
27
30
  def resolv_conf
31
+ return unless @config&.resolv_conf
32
+
28
33
  # We can’t really bind symlinks, so let’s resolve real path to resolv.conf, in case it is symlinked.
29
34
  source_resolv_conf = Pathname.new "/etc/resolv.conf"
30
35
  source_resolv_conf = source_resolv_conf.realpath
@@ -35,7 +40,7 @@ class Bwrap::Args::Network
35
40
 
36
41
  # Arguments to allow network connection inside sandbox.
37
42
  def share_net
38
- return unless @config.share_net
43
+ return unless @config&.share_net
39
44
 
40
45
  verb "Sharing network"
41
46
  @args.add :network, %w{ --share-net }
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bwrap/output"
4
+ require_relative "args"
5
+
6
+ # User related arguments.
7
+ class Bwrap::Args::User
8
+ include Bwrap::Output
9
+
10
+ # Instance of {Config}.
11
+ attr_writer :config
12
+
13
+ # @param args [Args] Args created by {Construct}
14
+ def initialize args
15
+ @args = args
16
+ end
17
+
18
+ # Arguments to create `/run/user/#{uid}`.
19
+ def create_user_dir
20
+ trace "Creating directory /run/user/#{uid}"
21
+ @args.add :user_dir, %W{ --dir /run/user/#{uid} }
22
+ end
23
+
24
+ # Arguments to bind necessary pulseaudio data for audio support.
25
+ def read_only_pulseaudio
26
+ return unless @config&.audio&.include? :pulseaudio
27
+
28
+ debug "Binding pulseaudio"
29
+ @args.add :audio, %W{ --ro-bind /run/user/#{uid}/pulse /run/user/#{uid}/pulse }
30
+ end
31
+
32
+ # Returns current user id.
33
+ private def uid
34
+ Process.uid
35
+ end
36
+ end
data/lib/bwrap/bwrap.rb CHANGED
@@ -12,14 +12,27 @@ require "bwrap/execution"
12
12
  # using given configuration.
13
13
  #
14
14
  # @see ::Bwrap Bwrap module for usage example
15
+ #
16
+ # TODO: Add some means to validate what a command would actually bind.
17
+ # Like, create a method like Bwrap#wrapped_command which would just return the command instead of executing it.
18
+ # Then add some instructions about this feature, as for security it is important to know what a thing actually does.
19
+ #
20
+ # TODO: There should be a proper shell feature, which binds some always necessary things for shells.
21
+ # Maybe by default only enough to run a command using a shell, i.e. running “true” would work,
22
+ # instead of needing to specify its path.
15
23
  class Bwrap::Bwrap
16
24
  include Bwrap::Execution
17
25
 
26
+ # @note When config is not given, alternative root directory can be specified
27
+ # with {#run_inside_root}.
28
+ #
18
29
  # @param config [Bwrap::Config] Configuration used to tailor bwrap
19
- def initialize config
30
+ def initialize config = nil
20
31
  # TODO: Ensure that costruct.rb and utilities it uses does not enforce Config to be valid object.
21
32
  # Create a test for that also. Well, as long as that makes sense. If it doesn’t work, validate
22
33
  # Config object to be valid here.
34
+ #
35
+ # Related to above, it is now optional, but a test still needs to be done.
23
36
  @config = config
24
37
  end
25
38
 
@@ -38,35 +51,58 @@ class Bwrap::Bwrap
38
51
  Bwrap::Output.handle_output_options options
39
52
  end
40
53
 
54
+ # @todo Proper documentation.
55
+ #
56
+ # @param command [String, Array] Command to be executed inside bwrap along with necessary arguments
57
+ # @param config [Bwrap::Config|nil] Configuration to tailor bwrap sandbox.
58
+ # Defaults to config given to {#initialize}
59
+ def build_bwrap_arguments command, config: nil
60
+ construct = Bwrap::Args::Construct.new
61
+ construct.command = command
62
+ construct.config = (config || @config)
63
+
64
+ construct.calculate
65
+ bwrap_args = construct.bwrap_arguments
66
+ @construct = construct
67
+
68
+ exec_command = [ "bwrap" ]
69
+ exec_command += bwrap_args
70
+ exec_command.append command
71
+ exec_command += @cli_args if @cli_args
72
+
73
+ exec_command
74
+ end
75
+
41
76
  # Binds and executes given command available on running system inside bwrap.
42
77
  #
43
78
  # If {Config#root} has been set, given executable is scanned for necessary
44
79
  # libraries and they are bound inside sandbox. This often reduces amount of
45
80
  # global binds, but some miscellaneous things may need custom handling.
46
81
  #
82
+ # `kwargs` are passed to {#execute}.
83
+ #
47
84
  # @see Config#features about binding bigger feature sets to sandbox.
48
85
  #
49
86
  # Executed command is constructed using configuration passed to constructor.
50
87
  # After execution has completed, bwrap will also shut down.
51
88
  #
52
89
  # @param command [String, Array] Command to be executed inside bwrap along with necessary arguments
90
+ # @option kwargs [Integer]
91
+ # log_callback (1)
92
+ # Semi-internal variable used to tailor caller in debug messages
53
93
  # @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
94
+ def run command, **kwargs
95
+ exec_command = build_bwrap_arguments command
58
96
 
59
- construct.calculate
60
- bwrap_args = construct.bwrap_arguments
97
+ # add 1 to log_callback so executing is shown to where this method is called at.
98
+ kwargs[:log_callback] ||= 1
99
+ kwargs[:log_callback] += 1
61
100
 
62
- exec_command = [ "bwrap" ]
63
- exec_command += bwrap_args
64
- exec_command.append command
65
- exec_command += @cli_args if @cli_args
101
+ result = execute exec_command, **kwargs
66
102
 
67
- execute exec_command
103
+ @construct.cleanup
68
104
 
69
- construct.cleanup
105
+ result
70
106
  end
71
107
 
72
108
  # Convenience method to executes a command that is inside bwrap.
@@ -75,12 +111,14 @@ class Bwrap::Bwrap
75
111
  #
76
112
  # Calling this method is equivalent to setting {Config#command_inside_root} to `true`.
77
113
  #
114
+ # `kwargs` are passed to {#execute}.
115
+ #
78
116
  # @note This may have a bit unintuitive usage, as most things are checked anyway, so this is not
79
117
  # akin to running something inside a chroot, but rather for convenience.
80
118
  #
81
119
  # @param command [String, Array] Command to be executed inside bwrap along with necessary arguments
82
120
  # @see #run to execute a command that needs to be bound to the sandbox.
83
- def run_inside_root command
121
+ def run_inside_root command, **kwargs
84
122
  if @config
85
123
  config = @config.dup
86
124
  config.command_inside_root = true
@@ -88,21 +126,15 @@ class Bwrap::Bwrap
88
126
  config = nil
89
127
  end
90
128
 
91
- construct = Bwrap::Args::Construct.new
92
- construct.command = command
93
- construct.config = config
94
-
95
- construct.calculate
96
- bwrap_args = construct.bwrap_arguments
129
+ # add 1 to log_callback so executing is shown to where this method is called at.
130
+ kwargs[:log_callback] ||= 1
131
+ kwargs[:log_callback] += 1
97
132
 
98
- exec_command = [ "bwrap" ]
99
- exec_command += bwrap_args
100
- exec_command.append command
101
- exec_command += @cli_args if @cli_args
133
+ exec_command = build_bwrap_arguments command, config: config
102
134
 
103
- execute exec_command
135
+ execute exec_command, **kwargs
104
136
 
105
- construct.cleanup
137
+ @construct.cleanup
106
138
  end
107
139
 
108
140
  # Parses global bwrap flags using Optimist.
data/lib/bwrap/config.rb CHANGED
@@ -17,6 +17,15 @@ require_relative "config/features"
17
17
  # Note that all attributes also have writers, even though they are not documented.
18
18
  #
19
19
  # @todo Add some documentation about syntax where necessary, like for #binaries_from.
20
+ #
21
+ # TODO: I don’t remember if I made bash feature, but maybe it should be done.
22
+ # It should automatically bind /bin and /usr/bin, with a flag to bind relevant sbin dirs.
23
+ # That is because most scripts needs stuff from there. Maybe my fish profile would give some
24
+ # useful basic stuff?
25
+ #
26
+ # TODO: Maybe I should have something like second-level configuration in variable “advanced” or similar,
27
+ # which allows controlling features that more directly maps to bwrap cli args? That way it would be easier
28
+ # to use only high-level features.
20
29
  class Bwrap::Config
21
30
  # Array of audio schemes usable inside chroot.
22
31
  #
@@ -31,10 +40,19 @@ class Bwrap::Config
31
40
  # @return [Boolean] `true` if executed command is inside sandbox
32
41
  attr_accessor :command_inside_root
33
42
 
34
- attr_accessor :extra_executables
43
+ # @return [Boolean] `true` if dummy devtmpfs should be mounted inside sandbox
44
+ attr_accessor :dev_mount
35
45
 
36
- # TODO: IIRC this doesn’t match the reality any more. So write correct documentation.
46
+ # Additional executables to bind to target.
47
+ #
48
+ # TODO: Implement this paragraph:
49
+ # If an executable given here is found from directory given to
50
+ # {#binaries_from=}, it is not bound to target, but only
51
+ # dependent libraries.
37
52
  #
53
+ # @return [#each] Array of executables to bind
54
+ attr_accessor :extra_executables
55
+
38
56
  # Causes libraries required by the executable given to {Bwrap#run} to be
39
57
  # mounted inside sandbox.
40
58
  #
@@ -42,8 +60,16 @@ class Bwrap::Config
42
60
  # using {#libdir_mounts=}
43
61
  #
44
62
  # @return [Boolean] true if Linux library loaders are mounted inside chroot
63
+ #
64
+ # TODO: Since this only causes given executable be scanned for dependencies,
65
+ # and not ”--bind / /”, this one should be deprecated and something like
66
+ # ”@config.bind_dependents = true” should be added as alias of this.
45
67
  attr_accessor :full_system_mounts
46
68
 
69
+ # If set to `true`, things like /dev/dri is bound to sandbox to enable usage
70
+ # of hardware video acceleration, for example.
71
+ attr_accessor :graphics_acceleration
72
+
47
73
  attr_accessor :hostname
48
74
 
49
75
  # Set to true if basic system directories, like /usr/lib and /usr/lib64,
@@ -54,6 +80,9 @@ class Bwrap::Config
54
80
  # Often it is enough to use {#full_system_mounts=} instead of binding all
55
81
  # system libraries using this flag.
56
82
  #
83
+ # It may also make sense to bind specific library directories
84
+ # using {#ro_binds=}.
85
+ #
57
86
  # @return [Boolean] true if libdirs are mounted to the chroot
58
87
  attr_accessor :libdir_mounts
59
88
 
@@ -71,9 +100,22 @@ class Bwrap::Config
71
100
  # Given file as bound as /etc/machine_id.
72
101
  attr_accessor :machine_id
73
102
 
103
+ # If set to truthy, /etc/resolv.conf will be bound to target.
104
+ attr_accessor :resolv_conf
105
+
74
106
  # @return [Boolean] true if network should be shared from host.
75
107
  attr_accessor :share_net
76
108
 
109
+ # Set to truthy to remove (see bwrap’s --unshare-all) all namespaces from
110
+ # target chroot.
111
+ #
112
+ # Defaults to true.
113
+ #
114
+ # TODO: Create more fine grained control for sharing logic than this one.
115
+ #
116
+ # @return [Boolean] true if all namespaces are tried to be removed from target.
117
+ attr_accessor :unshare_all
118
+
77
119
  # Name of the user inside chroot.
78
120
  #
79
121
  # This is optional and defaults to no user.
@@ -89,11 +131,11 @@ class Bwrap::Config
89
131
  #
90
132
  # Given paths are also added to PATH environment variable inside sandbox.
91
133
  #
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:
134
+ # Hint: At least on SUSE, many executables are symlinks to /etc/alternatives/*,
135
+ # which in turn symlinks to versioned executable under the same bindir.
136
+ # To use these executables, /etc/alternatives should also be bound:
95
137
  #
96
- # config.ro_binds["/etc/alternatives"] = "/etc/alternatives"
138
+ # config.ro_binds["/etc/alternatives"] = "/etc/alternatives"
97
139
  #
98
140
  # @return [Array] Paths to directories where binaries are looked from.
99
141
  attr_reader :binaries_from
@@ -113,10 +155,10 @@ class Bwrap::Config
113
155
 
114
156
  # @overload ro_binds
115
157
  # `Hash`[`Pathname`] => `Pathname` containing custom read-only binds.
116
- # @overload ro_binds=
117
- # Set given hash of paths to be bound with --ro-bind.
158
+ # @overload ro_binds= binds
159
+ # Set given Hash of paths to be bound with --ro-bind.
118
160
  #
119
- # Key is source path, value is destination path.
161
+ # Key of `binds` is source path, value is destination path.
120
162
  #
121
163
  # Given source paths must exist.
122
164
  attr_reader :ro_binds
@@ -139,6 +181,7 @@ class Bwrap::Config
139
181
  @env_paths = []
140
182
  @ro_binds = {}
141
183
  @tmpdir = Dir.tmpdir
184
+ @unshare_all = true
142
185
  end
143
186
 
144
187
  def binaries_from= array
@@ -0,0 +1 @@
1
+ # frozen_string_literal: true
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "execute"
4
+
5
+ # Actually performs the execution.
6
+ class Bwrap::Execution::Exec
7
+ # @param value [Boolean] True if execution should be skipped
8
+ attr_writer :dry_run
9
+
10
+ attr_writer :clear_env
11
+ attr_writer :config
12
+ attr_writer :direct_output
13
+ attr_writer :env
14
+ attr_writer :error
15
+ attr_writer :fail
16
+ attr_writer :log
17
+ attr_writer :rootcmd
18
+ attr_writer :wait
19
+
20
+ attr_reader :last_status
21
+
22
+ # @see Bwrap::Execution.do_execute
23
+ def initialize command
24
+ @command = command
25
+
26
+ @fail = true
27
+ @wait = true
28
+ @log = true
29
+ @direct_output = false
30
+ @env = nil
31
+ @clear_env = false
32
+ @error = nil
33
+ @config = nil
34
+ @rootcmd = nil
35
+ end
36
+
37
+ def log_callback= value
38
+ # Adds one since this is another method in the chain.
39
+ @log_callback = value + 1
40
+ end
41
+
42
+ def execute
43
+ # Default to two steps back in history. Hopefully this is correct amount...
44
+ log_callback = @log_callback || 1
45
+
46
+ command = Bwrap::Execution::Execute.format_command @command, rootcmd: @rootcmd, config: @config
47
+ Bwrap::Execution::Logging.handle_logging command, log_callback: log_callback, log: @log, dry_run: @dry_run
48
+ return if @dry_run || Bwrap::Execution::Execute.dry_run
49
+
50
+ Bwrap::Execution::Execute.open_pipes @direct_output
51
+
52
+ # If command is an array, there can’t be arrays inside the array.
53
+ # For convenience, the array is flattened here, so callers can construct commands more easily.
54
+ if command.respond_to? :flatten!
55
+ command.flatten!
56
+ end
57
+
58
+ env = @env || {}
59
+
60
+ # If command is string, splat operator (the *) does not do anything. If array, it expand the arguments.
61
+ # This causes spawning work correctly, as that’s how spawn() expects to have the arguments.
62
+ pid = spawn(env, *command, err: [ :child, :out ], out: Bwrap::Execution::Execute.w, unsetenv_others: @clear_env)
63
+
64
+ output = Bwrap::Execution::Execute.finish_execution(log: @log, wait: @wait, direct_output: @direct_output)
65
+ return pid unless @wait
66
+
67
+ # This is instant return, but allows us to have $?/$CHILD_STATUS set.
68
+ Process.wait pid
69
+ @last_status = $CHILD_STATUS
70
+
71
+ output = Bwrap::Execution::Execute.process_output output: output
72
+ Bwrap::Execution::Execute.handle_execution_fail fail: @fail, error: @error, output: output, command: command
73
+
74
+ output
75
+ ensure
76
+ Bwrap::Execution::Execute.clean_variables
77
+ end
78
+ end
@@ -22,24 +22,10 @@ class Bwrap::Execution::Execute
22
22
  attr_accessor :dry_run
23
23
  end
24
24
 
25
- # Formats given command for logging, depending on dry-run flag.
26
- def self.handle_logging command, log_callback:, log:, dry_run:
27
- # The debug message contents will always be evaluated, so can just do it like this.
28
- log_command = calculate_log_command command
29
-
30
- if dry_run || Bwrap::Execution::Execute.dry_run
31
- puts "Would execute “#{log_command.force_encoding("UTF-8")}” at #{caller_locations(log_callback, 1)[0]}"
32
- return
33
- end
34
-
35
- return unless log
36
-
37
- msg = "Executing “#{log_command.force_encoding("UTF-8")}” at #{caller_locations(log_callback, 1)[0]}"
38
- Bwrap::Output.debug_output msg
39
- end
40
-
41
- # @return formatted command.
42
- def self.format_command command, rootcmd:
25
+ # @param rootcmd [Array|Symbol|nil] Flags used to construct bubblewrap environment
26
+ # @param config [Bwrap::Config|nil] Configuration used to launch the command inside bubblewrap
27
+ # @return formatted command
28
+ def self.format_command command, rootcmd: nil, config: nil
43
29
  # Flatten the command if required, so nils can be converted in more of cases.
44
30
  # Flattenization is also done in executions, but they also take in account
45
31
  # for example rootcmd, so they probably should be done in addition to this one.
@@ -47,10 +33,16 @@ class Bwrap::Execution::Execute
47
33
  command.flatten!
48
34
  end
49
35
 
50
- replace_nils command
51
- return command if rootcmd.nil?
36
+ prepare_for_execution command
52
37
 
53
- prepend_rootcmd command, rootcmd: rootcmd
38
+ if config
39
+ bwrap = Bwrap::Bwrap.new config
40
+ bwrap.build_bwrap_arguments command
41
+ elsif rootcmd
42
+ prepend_rootcmd command, rootcmd: rootcmd
43
+ else
44
+ command
45
+ end
54
46
  end
55
47
 
56
48
  # Opens pipes for command output handling.
@@ -80,7 +72,8 @@ class Bwrap::Execution::Execute
80
72
  exception = Bwrap::Execution::ExecutionFailed.new "Command execution failed",
81
73
  command: command,
82
74
  output: output
83
- raise exception, caller
75
+
76
+ raise exception, exception.message, caller
84
77
  end
85
78
 
86
79
  # @note It makes sense for caller to just return if wait has been set and not check output.
@@ -119,31 +112,20 @@ class Bwrap::Execution::Execute
119
112
  $CHILD_STATUS.success?
120
113
  end
121
114
 
122
- # Used by `#handle_logging`.
123
- private_class_method def self.calculate_log_command command
124
- return command.dup unless command.respond_to?(:join)
125
-
126
- temp = command.dup
127
-
128
- # Wrap multi word arguments to quotes, for convenience.
129
- # NOTE: This is not exactly safe, but this is only a debugging aid anyway.
130
- temp.map! do |argument|
131
- if argument.include? " "
132
- escaped = argument.gsub '"', '\"'
133
- argument = %["#{escaped}"]
134
- end
135
- argument
136
- end
137
-
138
- temp.join(" ")
139
- end
140
-
141
115
  # If command is an `Array`, Replaces nil values with "".
142
- private_class_method def self.replace_nils command
116
+ #
117
+ # Also converts Pathnames to strings.
118
+ private_class_method def self.prepare_for_execution command
143
119
  return unless command.respond_to? :map!
144
120
 
145
121
  command.map! do |argument|
146
- argument.nil? && "" || argument
122
+ if argument.is_a? Pathname
123
+ argument.to_s
124
+ elsif argument.nil?
125
+ ""
126
+ else
127
+ argument
128
+ end
147
129
  end
148
130
  end
149
131