bwrap 1.1.1 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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