bwrap 1.0.0.pre.alpha1

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