bwrap 1.0.0.pre.alpha1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e4193a0724aceb154c2e9be0809366ed2998837beeeb577dc101a58e20dca97d
4
+ data.tar.gz: a927a6a6b8cb415f3983440ca0437b852582227f5b163f34143ffb375dc96a40
5
+ SHA512:
6
+ metadata.gz: f2f49d20cf15a331d34439d71ef038645bc2a5520407cdb519ea8f5e9e32db526dd7524ad35939acd8098a328d6a3c1c2542d5fde44d0adcfd8ae26f2313d5fb
7
+ data.tar.gz: ee295629e684ef280652e38a94e7731b6e3227a2bac1150a79da268f3d2972eaac889ac1928103edf9d022c84430898ebc53d008a6f181d412d3f1ede441d0a9
checksums.yaml.gz.sig ADDED
Binary file
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changes
2
+
3
+ ## 1.0.0-alpha1 (10.11.2021)
4
+
5
+ ## 0.1.0 (unreleased)
6
+
7
+ * First, currently unfinished and unreleased version. Work in Progress.
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright © 2021 Samu Voutilainen
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted, provided that the above
5
+ copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD
8
+ TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
9
+ FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT,
10
+ OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
11
+ DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
12
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
13
+ OF THIS SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # Bwrap
2
+
3
+ Framework to create commands for bwrap.
4
+
5
+ For now this is tailored to my needs, so this may or may not be of any use.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem "bwrap"
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install bwrap
22
+
23
+ ## Usage
24
+
25
+ For now this is under ongoing development, though semantic versioning will apply.
26
+
27
+ There is few different modules, especially execution stuff probably should be moved to its own gem.
28
+
29
+ Please see [API documentation](https://www.rubydoc.info/gems/bwrap) for usage instructions.
30
+
31
+ ## Contributing
32
+
33
+ Bug reports and pull requests are welcome at https://git.sr.ht/~smar/ruby-bwrap.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bwrap/version"
4
+
5
+ # bwrap argument related operations.
6
+ module Bwrap::Args
7
+ # Nya.
8
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "args"
4
+
5
+ # Bind arguments for bwrap.
6
+ module Bwrap::Args::Bind
7
+ # Arguments to bind /dev/dri from host to sandbox.
8
+ private def bind_dev_dri
9
+ %w{ --dev-bind /dev/dri /dev/dri }
10
+ end
11
+
12
+ # Arguments to bind /sys/dev/char from host to sandbox.
13
+ private def bind_sys_dev_char
14
+ %w{ --ro-bind /sys/dev/char /sys/dev/char }
15
+ end
16
+
17
+ # Arguments to bind /sys/devices/pci0000:00 from host to sandbox.
18
+ private def bind_pci_devices
19
+ %w{ --ro-bind /sys/devices/pci0000:00 /sys/devices/pci0000:00 }
20
+ end
21
+
22
+ # Arguments to bind home directory from sandbox directory (`#{@config.sandbox_directory}/home`)
23
+ # as `/home/#{@config.user}`.
24
+ #
25
+ # @note Requires @config.user to be set.
26
+ private def bind_home_directory
27
+ unless @config.user
28
+ raise "Tried to bind user directory without user being set."
29
+ end
30
+
31
+ home_directory = "#{@config.sandbox_directory}/home"
32
+
33
+ unless Dir.exist? home_directory
34
+ raise "Home directory #{home_directory} does not exist. You need to create it yourself."
35
+ end
36
+
37
+ @environment["HOME"] = "/home/#{@config.user}"
38
+
39
+ %W{ --bind #{home_directory} /home/#{@config.user} }
40
+ end
41
+
42
+ # Arguments to read-only bind whole system inside sandbox.
43
+ private def full_system_mounts
44
+ bindir_mounts = []
45
+ binaries_from = @config.binaries_from
46
+ binaries_from.each do |path|
47
+ bindir_mounts << "--ro-bind" << path << path
48
+ end
49
+ @environment["PATH"] = binaries_from.join(":")
50
+
51
+ libdir_mounts = %w{
52
+ --ro-bind /lib /lib
53
+ --ro-bind /lib64 /lib64
54
+ --ro-bind /usr/lib /usr/lib
55
+ --ro-bind /usr/lib64 /usr/lib64
56
+ }
57
+
58
+ system_mounts = bindir_mounts + libdir_mounts
59
+ if debug?
60
+ debug "Using following system mounts:\n" \
61
+ "#{system_mounts}\n" \
62
+ "(Odd is key, even is value)"
63
+ end
64
+ system_mounts
65
+ end
66
+
67
+ # These are something user can specify to do custom --ro-bind binds.
68
+ private def custom_read_only_binds
69
+ return [] unless @config.ro_binds
70
+
71
+ binds = []
72
+ @config.ro_binds.each do |source_path, destination_path|
73
+ binds << "--ro-bind" << source_path.to_s << destination_path.to_s
74
+ end
75
+
76
+ binds
77
+ end
78
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+
5
+ require "bwrap/output"
6
+ require_relative "bind"
7
+ require_relative "environment"
8
+ require_relative "machine_id"
9
+ require_relative "mount"
10
+
11
+ # Constructs arguments for bwrap execution.
12
+ class Bwrap::Args::Construct
13
+ include Bwrap::Output
14
+ include Bwrap::Args::Bind
15
+ include Bwrap::Args::Mount
16
+
17
+ attr_writer :config
18
+
19
+ # Constructs arguments for bwrap execution.
20
+ def construct_bwrap_args
21
+ @environment = Bwrap::Args::Environment.new
22
+ @environment.config = @config
23
+ @machine_id = Bwrap::Args::MachineId.new
24
+ @machine_id.config = @config
25
+
26
+ [
27
+ xauthority_args,
28
+ @machine_id.machine_id,
29
+ resolv_conf,
30
+ full_system_mounts,
31
+ custom_read_only_binds,
32
+ create_user_dir,
33
+ read_only_pulseaudio,
34
+ dev_mount,
35
+ bind_dev_dri,
36
+ bind_sys_dev_char,
37
+ bind_pci_devices,
38
+ proc_mount,
39
+ tmp_as_tmpfs,
40
+ bind_home_directory,
41
+ "--unshare-all",
42
+ share_net,
43
+ hostname,
44
+ @environment.environment_variables,
45
+ "--die-with-parent",
46
+ "--new-session"
47
+ ]
48
+ end
49
+
50
+ # Performs cleanup operations after execution.
51
+ def cleanup
52
+ @machine_id&.cleanup
53
+ end
54
+
55
+ # Arguments for generating .Xauthority file.
56
+ private def xauthority_args
57
+ xauth_args = %W{ --ro-bind #{Dir.home}/.Xauthority #{Dir.home}/.Xauthority }
58
+ debug "Binding following .Xauthority file: #{Dir.home}/.Xauthority"
59
+ xauth_args
60
+ end
61
+
62
+ # Arguments to read-only bind /etc/resolv.conf.
63
+ private def resolv_conf
64
+ #source_resolv_conf = "/etc/resolv.conf"
65
+ source_resolv_conf = "/run/netconfig/resolv.conf"
66
+ debug "Binding #{source_resolv_conf} as /etc/resolv.conf"
67
+ %w{ --ro-bind /run/netconfig/resolv.conf /etc/resolv.conf }
68
+ end
69
+
70
+ # Arguments to create `/run/user/#{uid}`.
71
+ private def create_user_dir
72
+ trace "Creating directory /run/user/#{uid}"
73
+ %W{ --dir /run/user/#{uid} }
74
+ end
75
+
76
+ # Arguments to bind necessary pulseaudio data for audio support.
77
+ private def read_only_pulseaudio
78
+ debug "Binding pulseaudio"
79
+ %W{ --ro-bind /run/user/#{uid}/pulse /run/user/#{uid}/pulse }
80
+ end
81
+
82
+ # Arguments to allow network connection inside sandbox.
83
+ private def share_net
84
+ verb "Sharing network"
85
+ %w{ --share-net }
86
+ end
87
+
88
+ # Arguments to set hostname to whatever is configured.
89
+ private def hostname
90
+ return unless @config.hostname
91
+
92
+ debug "Setting hostname to #{@config.hostname}"
93
+ %W{ --hostname #{@config.hostname} }
94
+ end
95
+
96
+ # Returns current user id.
97
+ private def uid
98
+ Process.uid
99
+ end
100
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bwrap/output"
4
+ require_relative "args"
5
+
6
+ # Environment variable calculation for bwrap.
7
+ class Bwrap::Args::Environment < Hash
8
+ include Bwrap::Output
9
+
10
+ # Instance of {Config}.
11
+ attr_writer :config
12
+
13
+ # Returns used environment variables.
14
+ def environment_variables
15
+ if debug?
16
+ debug "Passing following environment variables to bwrap:\n" \
17
+ "#{self}"
18
+ end
19
+
20
+ map do |key, value|
21
+ [ "--setenv", key, value ]
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ require "bwrap/output"
6
+ require_relative "args"
7
+
8
+ # Calculate machine id data.
9
+ class Bwrap::Args::MachineId
10
+ include Bwrap::Output
11
+
12
+ # Instance of {Config}.
13
+ attr_writer :config
14
+
15
+ # machine_id == :random
16
+ # Generates random machine id for each launch and sets it as /etc/machine_id.
17
+ # machine_id == :dummy
18
+ # Uses 10000000000000000000000000000000 as dummy machine id and sets it as /etc/machine_id.
19
+ # machine_id == true
20
+ # A file from `#{sandbox_directory}/machine_id` is bound as /etc/machine_id.
21
+ # machine_id.is_a? String
22
+ # Given file as bound as /etc/machine_id.
23
+ def machine_id
24
+ # Returning [] means that execute() will ignore this fully.
25
+ # Nil would be converted to empty string, causing spawn() to pass it as argument, causing
26
+ # bwrap to misbehave.
27
+ return [] unless @config.machine_id
28
+
29
+ machine_id = @config.machine_id
30
+
31
+ if machine_id == :random
32
+ random_machine_id
33
+ elsif machine_id == :dummy
34
+ dummy_machine_id
35
+ elsif machine_id == true
36
+ machine_id_inside_sandbox_dir @config.sandbox_directory
37
+ elsif machine_id.is_a? String
38
+ string_machine_id machine_id
39
+ end
40
+ end
41
+
42
+ # Removes opened temporary machine id file.
43
+ #
44
+ # Can be called safely even if no file is opened.
45
+ def cleanup
46
+ @machine_id_file&.unlink
47
+ end
48
+
49
+ # Generated random machine id.
50
+ private def random_machine_id
51
+ debug "Using random machine id as /etc/machine-id"
52
+
53
+ @machine_id_file = Tempfile.new "bwrap-random_machine_id-", @config.tmpdir
54
+ @machine_id_file.write SecureRandom.uuid.gsub("-", "")
55
+ @machine_id_file.flush
56
+
57
+ %W{ --ro-bind-data #{machine_id_file.fileno} /etc/machine-id }
58
+ end
59
+
60
+ # Uses `10000000000000000000000000000000` as machine id.
61
+ private def dummy_machine_id
62
+ debug "Using dummy machine id as /etc/machine-id"
63
+
64
+ @machine_id_file = Tempfile.new "bwrap-dummy_machine_id-", @config.tmpdir
65
+ @machine_id_file.write "10000000000000000000000000000000"
66
+ @machine_id_file.flush
67
+
68
+ %W{ --ro-bind-data #{@machine_id_file.fileno} /etc/machine-id }
69
+ end
70
+
71
+ # Uses given file as machine id.
72
+ private def string_machine_id machine_id_file
73
+ unless File.exist? machine_id_file
74
+ raise "Configured machine_id file #{machine_id_file} does not exist."
75
+ end
76
+
77
+ debug "Binding #{machine_id_file} as /etc/machine-id"
78
+ %W{ --ro-bind #{machine_id_file} /etc/machine-id }
79
+ end
80
+
81
+ # Uses file inside sandbox directory as machine id.
82
+ private def machine_id_inside_sandbox_dir sandbox_directory
83
+ machine_id_file = "#{sandbox_directory}/machine-id"
84
+
85
+ unless File.exist? machine_id_file
86
+ raise "#{machine_id_file} does not exist.\n" \
87
+ "Hint: you can generate dummy machine-id file with:\n" \
88
+ " echo 10000000000000000000000000000000 > #{machine_id_file}"
89
+ end
90
+
91
+ debug "Binding #{machine_id_file} as /etc/machine-id"
92
+ %W{ --ro-bind #{machine_id_file} /etc/machine-id }
93
+ end
94
+ end
95
+
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "args"
4
+
5
+ # Bind arguments for bwrap.
6
+ module Bwrap::Args::Mount
7
+ # Arguments for mounting devtmpfs to /dev.
8
+ private def dev_mount
9
+ debug "Mounting new devtmpfs to /dev"
10
+ %w{ --dev /dev }
11
+ end
12
+
13
+ # Arguments for mounting procfs to /proc.
14
+ private def proc_mount
15
+ debug "Mounting new procfs to /proc"
16
+ %w{ --proc /proc }
17
+ end
18
+
19
+ # Arguments for mounting tmpfs to /tmp.
20
+ private def tmp_as_tmpfs
21
+ debug "Mounting tmpfs to /tmp"
22
+ %w{ --tmpfs /tmp }
23
+ end
24
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bwrap/output"
4
+ require "bwrap/version"
5
+
6
+ # Represents configuration set by user for bwrap execution.
7
+ #
8
+ # Implementation NOTE: logger methods (debug, warn and so on) can’t be used here as logger
9
+ # isn’t initialized before this class.
10
+ class Bwrap::Config
11
+ attr_accessor :hostname
12
+
13
+ # What should be used as /etc/machine_id file.
14
+ #
15
+ # If not specified, no /etc/machine_id handling is done.
16
+ #
17
+ # machine_id == :random
18
+ # Generates random machine id for each launch and sets it as /etc/machine_id.
19
+ # machine_id == :dummy
20
+ # Uses 10000000000000000000000000000000 as dummy machine id and sets it as /etc/machine_id.
21
+ # machine_id == true
22
+ # A file from #{sandbox_directory}/machine_id is bound as /etc/machine_id.
23
+ # machine_id.is_a? String
24
+ # Given file as bound as /etc/machine_id.
25
+ attr_accessor :machine_id
26
+
27
+ attr_accessor :user
28
+
29
+ # Array of directories to be bind mounted and used to construct PATH environment variable.
30
+ attr_reader :binaries_from
31
+
32
+ attr_reader :sandbox_directory
33
+
34
+ # `Hash`[`Pathname`] => `Pathname` containing custom read-only binds.
35
+ attr_reader :ro_binds
36
+
37
+ # Path to temporary directory.
38
+ #
39
+ # Defaults to Dir.tmpdir.
40
+ attr_reader :tmpdir
41
+
42
+ def initialize
43
+ @binaries_from = []
44
+ @tmpdir = Dir.tmpdir
45
+ end
46
+
47
+ def binaries_from= array
48
+ @binaries_from = []
49
+ array.each do |path|
50
+ unless Dir.exist? path
51
+ raise "Path “#{path}” given to binaries_from does not exist."
52
+ end
53
+
54
+ @binaries_from << path
55
+ end
56
+ end
57
+
58
+ def sandbox_directory= directory
59
+ unless Dir.exist? directory
60
+ raise "Given sandbox directory #{directory} does not exist. Please create it beforehand and setup to your needs."
61
+ end
62
+
63
+ @sandbox_directory = directory
64
+ end
65
+
66
+ # Set given hash of paths to be bound with --ro-bind.
67
+ #
68
+ # Key is source path, value is destination path.
69
+ #
70
+ # Given source paths must exist.
71
+ def ro_binds= binds
72
+ @ro_binds = {}
73
+ binds.each do |source_path, destination_path|
74
+ source_path = Pathname.new source_path
75
+ unless source_path.exist?
76
+ raise "Given read only bind does not exist. Please check path “#{source_path}” is correct."
77
+ end
78
+
79
+ destination_path = Pathname.new destination_path
80
+
81
+ @ro_binds[source_path] = destination_path
82
+ end
83
+ end
84
+
85
+ # Sets given directory as temporary directory for certain operations.
86
+ #
87
+ # @note Requires `dir` to be path to existing directory.
88
+ # @raise [RuntimeError] If given directory does not exist
89
+ # @param dir Path to temporary directory
90
+ def tmpdir= dir
91
+ raise "Directory to be set as a temporary directory, “#{dir}”, does not exist." unless Dir.exist? dir
92
+
93
+ @tmpdir = dir
94
+ end
95
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: false
2
+
3
+ # force_encoding modifies string, so can’t freeze strings.
4
+
5
+ require "bwrap/output"
6
+ require_relative "execution"
7
+
8
+ # Methods that performs actual execution logic.
9
+ #
10
+ # @api internal
11
+ class Bwrap::Execution::Execute
12
+ include Bwrap::Output
13
+
14
+ class << self
15
+ # Can be used to access pipe where `Kernel#spawn` should write its data.
16
+ attr_reader :w
17
+
18
+ # Dry run flag from parent Execution module.
19
+ attr_accessor :dry_run
20
+ end
21
+
22
+ # Formats given command for logging, depending on dry-run flag.
23
+ def self.handle_logging command, log_callback:, log:, dry_run:
24
+ # The debug message contents will always be evaluated, so can just do it like this.
25
+ log_command = calculate_log_command command
26
+
27
+ if dry_run || Bwrap::Execution::Execute.dry_run
28
+ puts "Would execute “#{log_command.force_encoding("UTF-8")}” at #{caller_locations(log_callback, 1)[0]}"
29
+ return
30
+ end
31
+
32
+ return unless log
33
+
34
+ msg = "Executing “#{log_command.force_encoding("UTF-8")}” at #{caller_locations(log_callback, 1)[0]}"
35
+ Bwrap::Output.debug_output msg
36
+ end
37
+
38
+ # @return formatted command.
39
+ def self.format_command command, rootcmd:
40
+ replace_nils command
41
+ return command if rootcmd.nil?
42
+
43
+ prepend_rootcmd command, rootcmd: rootcmd
44
+ end
45
+
46
+ # Opens pipes for command output handling.
47
+ def self.open_pipes direct_output
48
+ @r, @w = IO.pipe
49
+ return unless direct_output
50
+
51
+ @pipe_w = @w
52
+ @w = $stdout
53
+ end
54
+
55
+ # Converts output to be UTF-8.
56
+ def self.process_output output:
57
+ # read_nonblock() uses read(), which always reads as ASCII-8BIT:
58
+ # In the case of an integer length, the resulting string is always in ASCII-8BIT encoding.
59
+ output.force_encoding("UTF-8").strip
60
+ end
61
+
62
+ # Checks whether execution failed and acts accordingly.
63
+ def self.handle_execution_fail fail:, error:, output:
64
+ return unless fail and !$CHILD_STATUS.success?
65
+
66
+ if error == :show and !output.empty?
67
+ Bwrap::Output.warn_output "Command failed with output:\n“#{output}”"
68
+ end
69
+ raise Bwrap::Execution::ExecutionFailed, "Command execution failed.", caller
70
+ end
71
+
72
+ # @note It makes sense for caller to just return if wait has been set and not check output.
73
+ #
74
+ # @return output from command or nil if execution should stop here.
75
+ def self.finish_execution log:, wait:, direct_output:
76
+ @w = @pipe_w if direct_output
77
+ @w.close
78
+ unless wait
79
+ @r.close
80
+ return # With wait == false, execute() stops here.
81
+ end
82
+ output = buffer_exec_output @r, log
83
+ @r.close
84
+
85
+ output
86
+ end
87
+
88
+ # Removes data in class instance variables after execution has completed either successfully or with an error.
89
+ def self.clean_variables
90
+ @w = nil
91
+ @r = nil
92
+ @pipe_w = nil
93
+ end
94
+
95
+ # Stub to instruct implementation in subclass.
96
+ def self.prepend_rootcmd command, rootcmd:
97
+ raise NotImplementedError, "If rootcmd execution is necessary, monkey patch Bwrap::Execution::Execute " \
98
+ "to add “self.prepend_rootcmd(command, rootcmd:)” method."
99
+ end
100
+
101
+ # Used by `#handle_logging`.
102
+ private_class_method def self.calculate_log_command command
103
+ return command.dup unless command.respond_to?(:join)
104
+
105
+ temp = command.dup
106
+
107
+ # Wrap multi word arguments to quotes, for convenience.
108
+ # NOTE: This is not exactly safe, but this is only a debugging aid anyway.
109
+ temp.map! do |argument|
110
+ if argument.include? " "
111
+ escaped = argument.gsub '"', '\"'
112
+ argument = %["#{escaped}"]
113
+ end
114
+ argument
115
+ end
116
+
117
+ temp.join(" ")
118
+ end
119
+
120
+ # If command is an `Array`, Replaces nil values with "".
121
+ private_class_method def self.replace_nils command
122
+ return unless command.respond_to? :map!
123
+
124
+ command.map! do |argument|
125
+ argument.nil? && "" || argument
126
+ end
127
+ end
128
+
129
+ # Used by Execution#execute, not useful for anything else.
130
+ private_class_method def self.buffer_exec_output read, log
131
+ buffer = String.new capacity: 1024
132
+ output = ""
133
+ until read.eof?
134
+ read.read_nonblock 1024, buffer
135
+ if log
136
+ print buffer if Bwrap::Output.verbose?
137
+ Bwrap::Output::Log.write_to_log buffer
138
+ end
139
+ output << buffer
140
+ end
141
+
142
+ output
143
+ end
144
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bwrap/version"
4
+
5
+ # Generic execution functionality.
6
+ module Bwrap::Execution
7
+ # Nyan.
8
+ end