Class: Sandbox Private

Inherits:
Object show all
Extended by:
Utils::Output::Mixin
Includes:
OS::Linux::Sandbox, OS::Mac::Sandbox, Utils::Output::Mixin
Defined in:
sandbox.rb

Overview

This class is part of a private API. This class may only be used in the Homebrew/brew repository. Third parties should avoid using this class if possible, as it may be removed or changed without warning.

Helper class for running a sub-process inside of a sandboxed environment.

Constant Summary collapse

PRIVILEGED_GROUPS =

This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.

Privileged groups that are expected to be able to use a working sandbox.

%w[admin staff root wheel].freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils::Output::Mixin

issue_reporting_message, odebug, odeprecated, odie, odisabled, ofail, oh1, oh1_title, ohai, ohai_title, onoe, opoo, opoo_outside_github_actions, opoo_without_github_actions_annotation, pretty_deprecated, pretty_disabled, pretty_duration, pretty_install_status, pretty_installed, pretty_uninstalled, pretty_unmarked, pretty_upgradable, pretty_warning

Methods included from OS::Linux::Sandbox

bubblewrap_candidate_paths, bubblewrap_executable, bubblewrap_executable!

Constructor Details

#initializevoid

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.



227
228
229
230
231
232
# File 'sandbox.rb', line 227

def initialize
  @profile = T.let(SandboxProfile.new, SandboxProfile)
  @failed = T.let(false, T::Boolean)
  @logfile = T.let(nil, T.nilable(T.any(String, Pathname)))
  @start = T.let(nil, T.nilable(Time))
end

Class Method Details

.available?Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:

  • (Boolean)


79
80
81
# File 'sandbox.rb', line 79

def self.available?
  false
end

.avoid_nested_sandboxing?Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Skip Homebrew's own sandbox when it is opted into via $HOMEBREW_AVOID_NESTED_SANDBOXING and already running inside another sandbox. The skip is only supported for an unprivileged user in a custom prefix; error out explaining why rather than silently sandboxing (and hanging) when either is not the case.

Returns:

  • (Boolean)


95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'sandbox.rb', line 95

def self.avoid_nested_sandboxing?
  return false unless Homebrew::EnvConfig.avoid_nested_sandboxing?
  return false unless nested_sandbox?

  if Homebrew.default_prefix?
    odie "Refusing to skip the sandbox: `$HOMEBREW_AVOID_NESTED_SANDBOXING` is set " \
         "inside another sandbox but Homebrew is using its default prefix " \
         "(#{HOMEBREW_PREFIX}); this is only supported in a custom prefix."
  end

  privileged_group = PRIVILEGED_GROUPS.find do |name|
    group = Etc.getgrnam(name)
    group && Process.groups.include?(group.gid)
  rescue ArgumentError
    false
  end
  if privileged_group
    odie "Refusing to skip the sandbox: `$HOMEBREW_AVOID_NESTED_SANDBOXING` is set " \
         "inside another sandbox but you are in the privileged `#{privileged_group}` " \
         "group; this is only supported for an unprivileged user."
  end

  true
end

.configuration_command_messagesArray<String>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



149
# File 'sandbox.rb', line 149

def self.configuration_command_messages = []

.configuration_commandsArray<String>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



146
# File 'sandbox.rb', line 146

def self.configuration_commands = []

.configure!void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



155
156
157
158
# File 'sandbox.rb', line 155

def self.configure!
  ensure_sandbox_installed!
  reset_state!
end

.ensure_sandbox_available!void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



124
125
126
127
128
# File 'sandbox.rb', line 124

def self.ensure_sandbox_available!
  return if available?

  raise failure_reason || "The sandbox is not available."
end

.ensure_sandbox_installed!(install_from_tests: false) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:

  • install_from_tests (Boolean) (defaults to: false)


121
# File 'sandbox.rb', line 121

def self.ensure_sandbox_installed!(install_from_tests: false); end

.executablePathname?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'sandbox.rb', line 194

def self.executable
  executable_candidate_paths.each do |path|
    begin
      candidate = Pathname.new(File.expand_path(executable_name, path))
    rescue ArgumentError
      next
    end

    next if !candidate.file? || !candidate.executable?
    next unless executable_usable?(candidate)

    return candidate
  end

  nil
end

.executable!Pathname

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



212
213
214
# File 'sandbox.rb', line 212

def self.executable!
  executable || raise("#{executable_name} is required to use the sandbox.")
end

.executable_candidate_paths::PATH

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



186
187
188
189
190
191
# File 'sandbox.rb', line 186

def self.executable_candidate_paths
  executable_path = Pathname.new(executable_name)
  return PATH.new(executable_path.dirname) if executable_path.absolute?

  PATH.new(ORIGINAL_PATHS, ENV.fetch("PATH"), HOMEBREW_ORIGINAL_BREW_FILE.dirname)
end

.executable_nameString

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:

Raises:

  • (NotImplementedError)


181
182
183
# File 'sandbox.rb', line 181

def self.executable_name
  raise NotImplementedError, "Sandbox is not implemented for this OS."
end

.executable_usable?(_candidate) ⇒ Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:

  • (Boolean)


217
218
219
# File 'sandbox.rb', line 217

def self.executable_usable?(_candidate)
  true
end

.failure_reasonString?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



136
137
138
139
140
# File 'sandbox.rb', line 136

def self.failure_reason
  return if state == :available

  "The sandbox is not available."
end

.nested_sandbox?Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Whether Homebrew is itself running inside another sandbox, which would make its own nested sandbox hang (macOS) or fail to start (Linux). Overridden per-OS.

Returns:

  • (Boolean)


87
# File 'sandbox.rb', line 87

def self.nested_sandbox? = false

.reset_state!void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



143
# File 'sandbox.rb', line 143

def self.reset_state!; end

.run_command(*command, writable_path:, deny_network: false) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'sandbox.rb', line 161

def self.run_command(*command, writable_path:, deny_network: false)
  ensure_sandbox_installed!
  ensure_sandbox_available!

  writable_path = Pathname(writable_path).expand_path
  if !writable_path.directory? || !writable_path.writable?
    raise UsageError,
          "`#{writable_path}` is not a writable directory."
  end

  writable_path = writable_path.realpath
  sandbox = new
  sandbox.allow_write_temp_and_cache
  sandbox.allow_write_path writable_path
  sandbox.deny_read_home
  sandbox.deny_all_network if deny_network
  sandbox.run "/bin/sh", "-c", "cd \"$1\" && shift && exec \"$@\"", "brew-sandbox-exec", writable_path, *command
end

.sandbox_install_commandString?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



152
# File 'sandbox.rb', line 152

def self.sandbox_install_command = nil

.stateSymbol

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



131
132
133
# File 'sandbox.rb', line 131

def self.state
  available? ? :available : :unavailable
end

.terminal_ioctl_requestInteger

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:

Raises:

  • (NotImplementedError)


222
223
224
# File 'sandbox.rb', line 222

def self.terminal_ioctl_request
  raise NotImplementedError, "Sandbox is not implemented for this OS."
end

Instance Method Details

#add_rule(allow:, operation:, filter: nil, modifier: nil) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:

  • allow (Boolean)
  • operation (String)
  • filter (SandboxPathFilter, nil) (defaults to: nil)
  • modifier (String, nil) (defaults to: nil)


243
244
245
246
# File 'sandbox.rb', line 243

def add_rule(allow:, operation:, filter: nil, modifier: nil)
  rule = SandboxRule.new(allow:, operation:, filter:, modifier:)
  @profile.add_rule(rule)
end

#allow_cvsvoid

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



416
417
418
# File 'sandbox.rb', line 416

def allow_cvs
  allow_write_path "#{Dir.home(ENV.fetch("USER"))}/.cvspass"
end

#allow_fossilvoid

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



421
422
423
424
# File 'sandbox.rb', line 421

def allow_fossil
  allow_write_path "#{Dir.home(ENV.fetch("USER"))}/.fossil"
  allow_write_path "#{Dir.home(ENV.fetch("USER"))}/.fossil-journal"
end

#allow_network(path:, type: :literal) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



453
454
455
# File 'sandbox.rb', line 453

def allow_network(path:, type: :literal)
  add_rule allow: true, operation: "network*", filter: path_filter(path, type)
end

#allow_read(path:, type: :literal) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



249
250
251
# File 'sandbox.rb', line 249

def allow_read(path:, type: :literal)
  add_rule allow: true, operation: "file-read*", filter: path_filter(path, type)
end

#allow_read_if_exists(path:, type: :literal) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



372
373
374
375
376
377
# File 'sandbox.rb', line 372

def allow_read_if_exists(path:, type: :literal)
  return unless path
  return unless File.exist?(path)

  allow_read path:, type:
end

#allow_write(path:, type: :literal) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



380
381
382
383
384
# File 'sandbox.rb', line 380

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)
  add_rule allow: true, operation: "file-write-mode", filter: path_filter(path, type)
end

#allow_write_cellar(formula) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



427
428
429
430
431
# File 'sandbox.rb', line 427

def allow_write_cellar(formula)
  allow_write_path formula.rack
  allow_write_path formula.etc
  allow_write_path formula.var
end

#allow_write_log(formula) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



437
438
439
# File 'sandbox.rb', line 437

def allow_write_log(formula)
  allow_write_path formula.logs
end

#allow_write_path(path) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



392
393
394
# File 'sandbox.rb', line 392

def allow_write_path(path)
  allow_write path:, type: :subpath
end

#allow_write_path_if_exists(path) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



397
398
399
400
401
402
# File 'sandbox.rb', line 397

def allow_write_path_if_exists(path)
  return unless path
  return unless File.exist?(path)

  allow_write_path path
end

#allow_write_temp_and_cachevoid

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



410
411
412
413
# File 'sandbox.rb', line 410

def allow_write_temp_and_cache
  allow_write_path HOMEBREW_TEMP
  allow_write_path HOMEBREW_CACHE
end

#allow_write_xcodevoid

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



434
# File 'sandbox.rb', line 434

def allow_write_xcode; end

#deny_all_networkvoid

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



458
459
460
# File 'sandbox.rb', line 458

def deny_all_network
  add_rule allow: false, operation: "network*"
end

#deny_read(path:, type: :literal) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



254
255
256
# File 'sandbox.rb', line 254

def deny_read(path:, type: :literal)
  add_rule allow: false, operation: "file-read*", filter: path_filter(path, type)
end

#deny_read_homevoid

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'sandbox.rb', line 264

def deny_read_home
  require "trust"

  home = Pathname(Dir.home(ENV.fetch("USER"))).realpath
  if [
    HOMEBREW_PREFIX,
    HOMEBREW_REPOSITORY,
    HOMEBREW_CACHE,
    HOMEBREW_LOGS,
    HOMEBREW_TEMP,
    ENV.fetch("GITHUB_WORKSPACE", nil),
    ENV.fetch("RUNNER_WORKSPACE", nil),
    ENV.fetch("RUNNER_TEMP", nil),
    Homebrew::Trust.trust_file,
    *home_write_paths.select { |path| File.exist?(path) },
  ].compact.any? do |path|
    path = Pathname(path)
    [path.expand_path, (path.realpath if path.exist?)].compact.any? { |pathname| pathname.ascend.include?(home) }
  end
    # When Homebrew or CI needs some `$HOME` paths to stay readable, deny only
    # well-known credential and personal-data paths instead of enumerating all
    # of `$HOME`.
    [
      ".ssh",
      ".aws",
      ".azure",
      ".boto",
      ".docker",
      ".config/fish",
      ".config/gh",
      ".config/gcloud",
      ".config/huggingface",
      ".config/pip",
      ".config/pypoetry",
      ".config/rclone",
      ".config/containers/auth.json",
      ".config/composer/auth.json",
      ".config/sops/age/keys.txt",
      ".gnupg",
      ".git-credentials",
      ".gitconfig",
      ".gsutil",
      ".kube",
      ".netrc",
      ".npmrc",
      ".yarnrc",
      ".yarnrc.yml",
      ".pnpmrc",
      ".bunfig.toml",
      ".pypirc",
      ".pip",
      ".poetry",
      ".local/share/pypoetry",
      ".gem/credentials",
      ".bundle/config",
      ".cargo/credentials",
      ".cargo/credentials.toml",
      ".composer/auth.json",
      ".condarc",
      ".m2/settings.xml",
      ".gradle/gradle.properties",
      ".sbt/1.0/credentials.sbt",
      ".terraform.d/credentials.tfrc.json",
      ".pulumi/credentials.json",
      ".oci/config",
      ".huggingface/token",
      ".cache/huggingface/token",
      ".claude",
      ".claude.json",
      ".kiro",
      ".bash_login",
      ".bash_logout",
      ".bash_profile",
      ".bashrc",
      ".bash_history",
      ".profile",
      ".zlogin",
      ".zlogout",
      ".zprofile",
      ".zshenv",
      ".zshrc",
      ".zsh_history",
      ".python_history",
      ".mysql_history",
      ".psql_history",
      ".env",
      ".env.local",
      "Documents",
      "Movies",
      "Music",
      "Pictures",
      "Library/Keychains",
      "Library/Mobile Documents",
      "Library/CloudStorage",
      "Dropbox",
      "Google Drive",
      "OneDrive",
    ].each do |path|
      path = home/path
      deny_read_path path if path.exist?
    end
    return
  end

  deny_read_path home
end

#deny_read_path(path) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



259
260
261
# File 'sandbox.rb', line 259

def deny_read_path(path)
  deny_read path:, type: :subpath
end

#deny_write(path:, type: :literal) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



387
388
389
# File 'sandbox.rb', line 387

def deny_write(path:, type: :literal)
  add_rule allow: false, operation: "file-write*", filter: path_filter(path, type)
end

#deny_write_homebrew_repositoryvoid

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



442
443
444
445
446
447
448
449
450
# File 'sandbox.rb', line 442

def deny_write_homebrew_repository
  deny_write path: HOMEBREW_ORIGINAL_BREW_FILE
  if HOMEBREW_PREFIX.to_s == HOMEBREW_REPOSITORY.to_s
    deny_write_path HOMEBREW_LIBRARY
    deny_write_path HOMEBREW_REPOSITORY/".git"
  else
    deny_write_path HOMEBREW_REPOSITORY
  end
end

#deny_write_path(path) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



405
406
407
# File 'sandbox.rb', line 405

def deny_write_path(path)
  deny_write path:, type: :subpath
end

#path_filter(path, type) ⇒ SandboxPathFilter

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:

  • (SandboxPathFilter)


560
561
562
563
564
565
566
567
568
569
570
571
572
573
# File 'sandbox.rb', line 560

def path_filter(path, type)
  # Any character is allowed: the OS-specific renderer quotes paths safely
  # (the seatbelt renderer escapes the `"` and `\` string delimiters; the
  # Linux sandbox passes each path as a separate argument), so even paths
  # with spaces, parentheses, quotes, backslashes or newlines are expressible.
  filter_path = case type
  when :regex   then path.to_s
  when :subpath, :literal
    expand_realpath(Pathname.new(path)).to_s
  else raise ArgumentError, "Invalid path filter type: #{type}"
  end

  SandboxPathFilter.new(path: filter_path, type:)
end

#record_log(file) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



235
236
237
# File 'sandbox.rb', line 235

def record_log(file)
  @logfile = file
end

#run(*args) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
# File 'sandbox.rb', line 463

def run(*args)
  Dir.mktmpdir("homebrew-sandbox", HOMEBREW_TEMP) do |tmpdir|
    allow_network path: File.join(tmpdir, "socket"), type: :literal if allow_network_for_error_pipe?
    @start = T.let(Time.now, T.nilable(Time))

    begin
      command = sandbox_command(args, tmpdir)
      # 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
        end

        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(self.class.terminal_ioctl_request, 0) # Make this the controlling terminal.

              ensure_child_tty_available

              # Move into a non-denied directory before `exec` so subsequent
              # `getcwd(3)` calls (which walk every parent) never cross a
              # `deny_read_home` path inherited from the caller's CWD.
              Dir.chdir(tmpdir)

              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)
        end

        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
        end
      end
    rescue
      @failed = true
      raise
    ensure
      record_sandbox_log
    end
  end
end