| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  | # typed: strict | 
					
						
							| 
									
										
										
										
											2019-04-19 15:38:03 +09:00
										 |  |  | # frozen_string_literal: true | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  | require "erb" | 
					
						
							| 
									
										
										
										
											2021-08-24 14:29:17 +01:00
										 |  |  | require "io/console" | 
					
						
							|  |  |  | require "pty" | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  | require "tempfile" | 
					
						
							| 
									
										
										
										
											2024-08-28 03:45:26 +01:00
										 |  |  | require "utils/fork" | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-19 07:02:01 +02:00
										 |  |  | # Helper class for running a sub-process inside of a sandboxed environment. | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  | class Sandbox | 
					
						
							| 
									
										
										
										
											2019-04-19 15:38:03 +09:00
										 |  |  |   SANDBOX_EXEC = "/usr/bin/sandbox-exec" | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-28 03:45:26 +01:00
										 |  |  |   # This is defined in the macOS SDK but Ruby unfortunately does not expose it. | 
					
						
							|  |  |  |   # This value can be found by compiling a C program that prints TIOCSCTTY. | 
					
						
							|  |  |  |   # The value is different on Linux but that's not a problem as we only support macOS in this file. | 
					
						
							|  |  |  |   TIOCSCTTY = 0x20007461
 | 
					
						
							|  |  |  |   private_constant :TIOCSCTTY | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-20 12:03:48 +02:00
										 |  |  |   sig { returns(T::Boolean) } | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |   def self.available? | 
					
						
							| 
									
										
										
										
											2022-11-20 14:27:28 -08:00
										 |  |  |     false | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-20 12:03:48 +02:00
										 |  |  |   sig { void } | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |   def initialize | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |     @profile = T.let(SandboxProfile.new, SandboxProfile) | 
					
						
							|  |  |  |     @failed = T.let(false, T::Boolean) | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { params(file: T.any(String, Pathname)).void } | 
					
						
							| 
									
										
										
										
											2015-04-16 21:41:59 +08:00
										 |  |  |   def record_log(file) | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |     @logfile = T.let(file, T.nilable(T.any(String, Pathname))) | 
					
						
							| 
									
										
										
										
											2015-04-16 21:41:59 +08:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { params(allow: T::Boolean, operation: String, filter: T.nilable(String), modifier: T.nilable(String)).void } | 
					
						
							|  |  |  |   def add_rule(allow:, operation:, filter: nil, modifier: nil) | 
					
						
							|  |  |  |     rule = SandboxRule.new(allow:, operation:, filter:, modifier:) | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |     @profile.add_rule(rule) | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { params(path: T.any(String, Pathname), type: Symbol).void } | 
					
						
							|  |  |  |   def allow_write(path:, type: :literal) | 
					
						
							|  |  |  |     add_rule allow: true, operation: "file-write*", filter: path_filter(path, type) | 
					
						
							|  |  |  |     add_rule allow: true, operation: "file-write-setugid", filter: path_filter(path, type) | 
					
						
							| 
									
										
										
										
											2024-07-13 15:58:41 -04:00
										 |  |  |     add_rule allow: true, operation: "file-write-mode", filter: path_filter(path, type) | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { params(path: T.any(String, Pathname), type: Symbol).void } | 
					
						
							|  |  |  |   def deny_write(path:, type: :literal) | 
					
						
							|  |  |  |     add_rule allow: false, operation: "file-write*", filter: path_filter(path, type) | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { params(path: T.any(String, Pathname)).void } | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |   def allow_write_path(path) | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |     allow_write path:, type: :subpath | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { params(path: T.any(String, Pathname)).void } | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |   def deny_write_path(path) | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |     deny_write path:, type: :subpath | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { void } | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |   def allow_write_temp_and_cache | 
					
						
							|  |  |  |     allow_write_path "/private/tmp" | 
					
						
							| 
									
										
										
										
											2015-08-25 17:34:52 +01:00
										 |  |  |     allow_write_path "/private/var/tmp" | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |     allow_write path: "^/private/var/folders/[^/]+/[^/]+/[C,T]/", type: :regex | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |     allow_write_path HOMEBREW_TEMP | 
					
						
							|  |  |  |     allow_write_path HOMEBREW_CACHE | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { void } | 
					
						
							| 
									
										
										
										
											2018-07-01 23:35:29 +02:00
										 |  |  |   def allow_cvs | 
					
						
							| 
									
										
										
										
											2020-11-23 17:31:17 +01:00
										 |  |  |     allow_write_path "#{Dir.home(ENV.fetch("USER"))}/.cvspass" | 
					
						
							| 
									
										
										
										
											2018-07-01 23:35:29 +02:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { void } | 
					
						
							| 
									
										
										
										
											2018-07-01 23:35:29 +02:00
										 |  |  |   def allow_fossil | 
					
						
							| 
									
										
										
										
											2020-11-23 17:31:17 +01:00
										 |  |  |     allow_write_path "#{Dir.home(ENV.fetch("USER"))}/.fossil" | 
					
						
							|  |  |  |     allow_write_path "#{Dir.home(ENV.fetch("USER"))}/.fossil-journal" | 
					
						
							| 
									
										
										
										
											2018-07-01 23:35:29 +02:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { params(formula: Formula).void } | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |   def allow_write_cellar(formula) | 
					
						
							|  |  |  |     allow_write_path formula.rack | 
					
						
							|  |  |  |     allow_write_path formula.etc | 
					
						
							|  |  |  |     allow_write_path formula.var | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-08-25 17:34:52 +01:00
										 |  |  |   # Xcode projects expect access to certain cache/archive dirs. | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { void } | 
					
						
							| 
									
										
										
										
											2015-08-25 17:34:52 +01:00
										 |  |  |   def allow_write_xcode | 
					
						
							| 
									
										
										
										
											2020-11-23 17:31:17 +01:00
										 |  |  |     allow_write_path "#{Dir.home(ENV.fetch("USER"))}/Library/Developer" | 
					
						
							| 
									
										
										
										
											2024-01-12 15:37:24 +01:00
										 |  |  |     allow_write_path "#{Dir.home(ENV.fetch("USER"))}/Library/Caches/org.swift.swiftpm" | 
					
						
							| 
									
										
										
										
											2015-08-25 17:34:52 +01:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { params(formula: Formula).void } | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |   def allow_write_log(formula) | 
					
						
							| 
									
										
										
										
											2015-04-25 22:07:06 -04:00
										 |  |  |     allow_write_path formula.logs | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { void } | 
					
						
							| 
									
										
										
										
											2016-09-23 08:26:49 +01:00
										 |  |  |   def deny_write_homebrew_repository | 
					
						
							| 
									
										
										
										
											2025-01-07 17:40:18 +00:00
										 |  |  |     deny_write path: HOMEBREW_ORIGINAL_BREW_FILE | 
					
						
							| 
									
										
										
										
											2020-11-09 20:09:16 +11:00
										 |  |  |     if HOMEBREW_PREFIX.to_s == HOMEBREW_REPOSITORY.to_s | 
					
						
							| 
									
										
										
										
											2016-09-23 08:26:49 +01:00
										 |  |  |       deny_write_path HOMEBREW_LIBRARY | 
					
						
							|  |  |  |       deny_write_path HOMEBREW_REPOSITORY/".git" | 
					
						
							| 
									
										
										
										
											2020-11-09 20:09:16 +11:00
										 |  |  |     else | 
					
						
							|  |  |  |       deny_write_path HOMEBREW_REPOSITORY | 
					
						
							| 
									
										
										
										
											2016-09-23 08:26:49 +01:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2015-04-23 12:33:54 +08:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-13 00:21:34 -04:00
										 |  |  |   sig { params(path: T.any(String, Pathname), type: Symbol).void } | 
					
						
							|  |  |  |   def allow_network(path:, type: :literal) | 
					
						
							|  |  |  |     add_rule allow: true, operation: "network*", filter: path_filter(path, type) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   sig { params(path: T.any(String, Pathname), type: Symbol).void } | 
					
						
							|  |  |  |   def deny_network(path:, type: :literal) | 
					
						
							|  |  |  |     add_rule allow: false, operation: "network*", filter: path_filter(path, type) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   sig { void } | 
					
						
							|  |  |  |   def allow_all_network | 
					
						
							|  |  |  |     add_rule allow: true, operation: "network*" | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   sig { void } | 
					
						
							|  |  |  |   def deny_all_network | 
					
						
							|  |  |  |     add_rule allow: false, operation: "network*" | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { params(args: T.any(String, Pathname)).void } | 
					
						
							| 
									
										
										
										
											2024-08-28 03:45:26 +01:00
										 |  |  |   def run(*args) | 
					
						
							|  |  |  |     Dir.mktmpdir("homebrew-sandbox", HOMEBREW_TEMP) do |tmpdir| | 
					
						
							|  |  |  |       allow_network path: File.join(tmpdir, "socket"), type: :literal # Make sure we have access to the error pipe. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       seatbelt = File.new(File.join(tmpdir, "homebrew.sb"), "wx") | 
					
						
							|  |  |  |       seatbelt.write(@profile.dump) | 
					
						
							|  |  |  |       seatbelt.close | 
					
						
							|  |  |  |       @start = T.let(Time.now, T.nilable(Time)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       begin | 
					
						
							|  |  |  |         command = [SANDBOX_EXEC, "-f", seatbelt.path, *args] | 
					
						
							|  |  |  |         # Start sandbox in a pseudoterminal to prevent access of the parent terminal. | 
					
						
							|  |  |  |         PTY.open do |controller, worker| | 
					
						
							|  |  |  |           # Set the PTY's window size to match the parent terminal. | 
					
						
							|  |  |  |           # Some formula tests are sensitive to the terminal size and fail if this is not set. | 
					
						
							|  |  |  |           winch = proc do |_sig| | 
					
						
							|  |  |  |             controller.winsize = if $stdout.tty? | 
					
						
							|  |  |  |               # We can only use IO#winsize if the IO object is a TTY. | 
					
						
							|  |  |  |               $stdout.winsize | 
					
						
							|  |  |  |             else | 
					
						
							|  |  |  |               # Otherwise, default to tput, if available. | 
					
						
							|  |  |  |               # This relies on ncurses rather than the system's ioctl. | 
					
						
							|  |  |  |               [Utils.popen_read("tput", "lines").to_i, Utils.popen_read("tput", "cols").to_i] | 
					
						
							|  |  |  |             end | 
					
						
							| 
									
										
										
										
											2021-09-01 16:06:07 +01:00
										 |  |  |           end | 
					
						
							| 
									
										
										
										
											2022-01-25 16:22:00 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-28 03:45:26 +01:00
										 |  |  |           write_to_pty = proc do | 
					
						
							|  |  |  |             # Don't hang if stdin is not able to be used - throw EIO instead. | 
					
						
							|  |  |  |             old_ttin = trap(:TTIN, "IGNORE") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # Update the window size whenever the parent terminal's window size changes. | 
					
						
							|  |  |  |             old_winch = trap(:WINCH, &winch) | 
					
						
							|  |  |  |             winch.call(nil) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             stdin_thread = Thread.new do | 
					
						
							|  |  |  |               IO.copy_stream($stdin, controller) | 
					
						
							|  |  |  |             rescue Errno::EIO | 
					
						
							|  |  |  |               # stdin is unavailable - move on. | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             stdout_thread = Thread.new do | 
					
						
							|  |  |  |               controller.each_char { |c| print(c) } | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             Utils.safe_fork(directory: tmpdir, yield_parent: true) do |error_pipe| | 
					
						
							|  |  |  |               if error_pipe | 
					
						
							|  |  |  |                 # Child side | 
					
						
							|  |  |  |                 Process.setsid | 
					
						
							|  |  |  |                 controller.close | 
					
						
							|  |  |  |                 worker.ioctl(TIOCSCTTY, 0) # Make this the controlling terminal. | 
					
						
							|  |  |  |                 File.open("/dev/tty", Fcntl::O_WRONLY).close # Workaround for https://developer.apple.com/forums/thread/663632 | 
					
						
							|  |  |  |                 worker.close_on_exec = true | 
					
						
							|  |  |  |                 exec(*command, in: worker, out: worker, err: worker) # And map everything to the PTY. | 
					
						
							|  |  |  |               else | 
					
						
							|  |  |  |                 # Parent side | 
					
						
							|  |  |  |                 worker.close | 
					
						
							|  |  |  |               end | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  |           rescue ChildProcessError => e | 
					
						
							|  |  |  |             raise ErrorDuringExecution.new(command, status: e.status) | 
					
						
							|  |  |  |           ensure | 
					
						
							|  |  |  |             stdin_thread&.kill | 
					
						
							|  |  |  |             stdout_thread&.kill | 
					
						
							|  |  |  |             trap(:TTIN, old_ttin) | 
					
						
							|  |  |  |             trap(:WINCH, old_winch) | 
					
						
							| 
									
										
										
										
											2022-01-25 16:22:00 +00:00
										 |  |  |           end | 
					
						
							| 
									
										
										
										
											2021-08-25 19:56:12 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-28 03:45:26 +01:00
										 |  |  |           if $stdin.tty? | 
					
						
							|  |  |  |             # If stdin is a TTY, use io.raw to set stdin to a raw, passthrough | 
					
						
							|  |  |  |             # mode while we copy the input/output of the process spawned in the | 
					
						
							|  |  |  |             # PTY. After we've finished copying to/from the PTY process, io.raw | 
					
						
							|  |  |  |             # will restore the stdin TTY to its original state. | 
					
						
							|  |  |  |             begin | 
					
						
							|  |  |  |               # Ignore SIGTTOU as setting raw mode will hang if the process is in the background. | 
					
						
							|  |  |  |               old_ttou = trap(:TTOU, "IGNORE") | 
					
						
							|  |  |  |               $stdin.raw(&write_to_pty) | 
					
						
							|  |  |  |             ensure | 
					
						
							|  |  |  |               trap(:TTOU, old_ttou) | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  |           else | 
					
						
							|  |  |  |             write_to_pty.call | 
					
						
							| 
									
										
										
										
											2022-01-25 16:22:00 +00:00
										 |  |  |           end | 
					
						
							| 
									
										
										
										
											2021-09-06 22:27:43 -07:00
										 |  |  |         end | 
					
						
							| 
									
										
										
										
											2024-08-28 03:45:26 +01:00
										 |  |  |       rescue | 
					
						
							|  |  |  |         @failed = true | 
					
						
							|  |  |  |         raise | 
					
						
							|  |  |  |       ensure | 
					
						
							|  |  |  |         sleep 0.1 # wait for a bit to let syslog catch up the latest events. | 
					
						
							|  |  |  |         syslog_args = [ | 
					
						
							|  |  |  |           "-F", "$((Time)(local)) $(Sender)[$(PID)]: $(Message)", | 
					
						
							|  |  |  |           "-k", "Time", "ge", @start.to_i.to_s, | 
					
						
							|  |  |  |           "-k", "Message", "S", "deny", | 
					
						
							|  |  |  |           "-k", "Sender", "kernel", | 
					
						
							|  |  |  |           "-o", | 
					
						
							|  |  |  |           "-k", "Time", "ge", @start.to_i.to_s, | 
					
						
							|  |  |  |           "-k", "Message", "S", "deny", | 
					
						
							|  |  |  |           "-k", "Sender", "sandboxd" | 
					
						
							|  |  |  |         ] | 
					
						
							|  |  |  |         logs = Utils.popen_read("syslog", *syslog_args) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # These messages are confusing and non-fatal, so don't report them. | 
					
						
							|  |  |  |         logs = logs.lines.grep_v(/^.*Python\(\d+\) deny file-write.*pyc$/).join | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         unless logs.empty? | 
					
						
							|  |  |  |           if @logfile | 
					
						
							|  |  |  |             File.open(@logfile, "w") do |log| | 
					
						
							|  |  |  |               log.write logs | 
					
						
							|  |  |  |               log.write "\nWe use time to filter sandbox log. Therefore, unrelated logs may be recorded.\n" | 
					
						
							|  |  |  |             end | 
					
						
							| 
									
										
										
										
											2020-11-23 17:31:17 +01:00
										 |  |  |           end | 
					
						
							| 
									
										
										
										
											2015-08-28 17:30:14 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-28 03:45:26 +01:00
										 |  |  |           if @failed && Homebrew::EnvConfig.verbose? | 
					
						
							|  |  |  |             ohai "Sandbox Log", logs | 
					
						
							|  |  |  |             $stdout.flush # without it, brew test-bot would fail to catch the log | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2020-11-23 17:31:17 +01:00
										 |  |  |         end | 
					
						
							| 
									
										
										
										
											2015-08-28 17:30:14 +08:00
										 |  |  |       end | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-13 14:39:53 -04:00
										 |  |  |   # @api private | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   sig { params(path: T.any(String, Pathname), type: Symbol).returns(String) } | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |   def path_filter(path, type) | 
					
						
							| 
									
										
										
										
											2024-07-31 12:10:51 -04:00
										 |  |  |     invalid_char = ['"', "'", "(", ")", "\n", "\\"].find do |c| | 
					
						
							| 
									
										
										
										
											2024-07-13 14:39:53 -04:00
										 |  |  |       path.to_s.include?(c) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     raise ArgumentError, "Invalid character #{invalid_char} in path: #{path}" if invalid_char | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |     case type | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |     when :regex   then "regex #\"#{path}\"" | 
					
						
							|  |  |  |     when :subpath then "subpath \"#{expand_realpath(Pathname.new(path))}\"" | 
					
						
							|  |  |  |     when :literal then "literal \"#{expand_realpath(Pathname.new(path))}\"" | 
					
						
							|  |  |  |     else raise ArgumentError, "Invalid path filter type: #{type}" | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-13 14:39:53 -04:00
										 |  |  |   private | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   sig { params(path: Pathname).returns(Pathname) } | 
					
						
							|  |  |  |   def expand_realpath(path) | 
					
						
							|  |  |  |     raise unless path.absolute? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     path.exist? ? path.realpath : expand_realpath(path.parent)/path.basename | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   class SandboxRule | 
					
						
							|  |  |  |     sig { returns(T::Boolean) } | 
					
						
							|  |  |  |     attr_reader :allow | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { returns(String) } | 
					
						
							|  |  |  |     attr_reader :operation | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { returns(T.nilable(String)) } | 
					
						
							|  |  |  |     attr_reader :filter | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { returns(T.nilable(String)) } | 
					
						
							|  |  |  |     attr_reader :modifier | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sig { params(allow: T::Boolean, operation: String, filter: T.nilable(String), modifier: T.nilable(String)).void } | 
					
						
							|  |  |  |     def initialize(allow:, operation:, filter:, modifier:) | 
					
						
							|  |  |  |       @allow = allow | 
					
						
							|  |  |  |       @operation = operation | 
					
						
							|  |  |  |       @filter = filter | 
					
						
							|  |  |  |       @modifier = modifier | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |   private_constant :SandboxRule | 
					
						
							| 
									
										
										
										
											2015-04-13 18:05:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-19 07:02:01 +02:00
										 |  |  |   # Configuration profile for a sandbox. | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |   class SandboxProfile | 
					
						
							| 
									
										
										
										
											2019-04-19 15:38:03 +09:00
										 |  |  |     SEATBELT_ERB = <<~ERB | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |       (version 1) | 
					
						
							|  |  |  |       (debug deny) ; log all denied operations to /var/log/system.log | 
					
						
							|  |  |  |       <%= rules.join("\n") %> | 
					
						
							|  |  |  |       (allow file-write* | 
					
						
							| 
									
										
										
										
											2015-05-10 17:39:53 +08:00
										 |  |  |           (literal "/dev/ptmx") | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |           (literal "/dev/dtracehelper") | 
					
						
							|  |  |  |           (literal "/dev/null") | 
					
						
							| 
									
										
										
										
											2017-07-11 01:47:36 -07:00
										 |  |  |           (literal "/dev/random") | 
					
						
							| 
									
										
										
										
											2015-08-27 21:25:27 -07:00
										 |  |  |           (literal "/dev/zero") | 
					
						
							| 
									
										
										
										
											2015-05-10 17:39:53 +08:00
										 |  |  |           (regex #"^/dev/fd/[0-9]+$") | 
					
						
							| 
									
										
										
										
											2019-10-07 14:51:33 +01:00
										 |  |  |           (regex #"^/dev/tty[a-z0-9]*$") | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |           ) | 
					
						
							| 
									
										
										
										
											2020-06-06 19:12:12 +01:00
										 |  |  |       (deny file-write*) ; deny non-allowlist file write operations | 
					
						
							| 
									
										
										
										
											2024-07-13 16:28:17 -04:00
										 |  |  |       (deny file-write-setugid) ; deny non-allowlist file write SUID/SGID operations | 
					
						
							| 
									
										
										
										
											2024-07-13 15:23:39 -04:00
										 |  |  |       (deny file-write-mode) ; deny non-allowlist file write mode operations | 
					
						
							| 
									
										
										
										
											2015-09-15 11:46:56 +08:00
										 |  |  |       (allow process-exec | 
					
						
							|  |  |  |           (literal "/bin/ps") | 
					
						
							|  |  |  |           (with no-sandbox) | 
					
						
							|  |  |  |           ) ; allow certain processes running without sandbox | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |       (allow default) ; allow everything else | 
					
						
							| 
									
										
										
										
											2018-07-11 15:17:40 +02:00
										 |  |  |     ERB | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |     sig { returns(T::Array[String]) } | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |     attr_reader :rules | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-20 12:03:48 +02:00
										 |  |  |     sig { void } | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |     def initialize | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |       @rules = T.let([], T::Array[String]) | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |     sig { params(rule: SandboxRule).void } | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |     def add_rule(rule) | 
					
						
							| 
									
										
										
										
											2019-04-18 17:33:02 +09:00
										 |  |  |       s = +"(" | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |       s << (rule.allow ? "allow" : "deny") | 
					
						
							|  |  |  |       s << " #{rule.operation}" | 
					
						
							|  |  |  |       s << " (#{rule.filter})" if rule.filter | 
					
						
							|  |  |  |       s << " (with #{rule.modifier})" if rule.modifier | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |       s << ")" | 
					
						
							| 
									
										
										
										
											2019-04-18 17:33:02 +09:00
										 |  |  |       @rules << s.freeze | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 20:36:57 -04:00
										 |  |  |     sig { returns(String) } | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  |     def dump | 
					
						
							|  |  |  |       ERB.new(SEATBELT_ERB).result(binding) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2020-08-19 07:02:01 +02:00
										 |  |  |   private_constant :SandboxProfile | 
					
						
							| 
									
										
										
										
											2015-04-09 17:42:54 +08:00
										 |  |  | end | 
					
						
							| 
									
										
										
										
											2022-11-20 14:27:28 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  | require "extend/os/sandbox" |