Skip to content

Conversation

@MUNJETIJAYANTHKRISHNA
Copy link

This Pull Request introduces a Dependency Cooldown mechanism to mitigate specific classes of supply chain attacks, aligning with the security objectives outlined in #21129.

Feature Overview
Installation of newly published formulae will be subject to a 5-minute (300 seconds) mandatory delay. This delay acts as a critical time barrier against automated dependency injection attacks (e.g., typosquatting, key compromise exploitation).

Verification (Command Syntax):
Normal Install (Triggers Cooldown): brew install
Bypass Cooldown (Installs Immediately): brew install --no-cooldown

Copilot AI review requested due to automatic review settings November 25, 2025 07:24
Copilot finished reviewing on behalf of MUNJETIJAYANTHKRISHNA November 25, 2025 07:30
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a dependency cooldown security feature that introduces a 5-minute mandatory delay before installing newly published formulae to mitigate supply chain attacks like typosquatting and key compromise exploitation. The implementation adds a --no-cooldown flag to bypass the security check when needed.

Key Changes

  • Added git_commit_time method to Formula class to retrieve Git commit timestamps for cooldown calculation
  • Implemented cooldown logic in FormulaInstaller that pauses installation for formulae published within the last 5 minutes
  • Added --no-cooldown CLI flag to allow users to bypass the security delay

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 15 comments.

File Description
Library/Homebrew/formula_installer.rb Adds cooldown constant, no_cooldown parameter throughout installer, and implements the 5-minute delay logic in the install method
Library/Homebrew/formula.rb Implements git_commit_time method to retrieve last commit time for formula files and adds necessary require statements
Library/Homebrew/cmd/install.rb Adds --no-cooldown switch to CLI interface and passes the flag to formula installers

Critical Issues Found:

  • Utils::Git.last_commit_time method does not exist and must be implemented
  • Install.formula_installers method doesn't accept the no_cooldown parameter, breaking the feature
  • Missing no_cooldown parameter in fetch_dependency method causing inconsistent behavior
  • Potential integer truncation bug in wait time calculation
  • No test coverage for this security-critical feature

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

skip_post_install:          args.skip_post_install?,
skip_link:                  args.skip_link?,
# ADDED for Dependency Cooldown: pass the flag down to the installer
no_cooldown: args.no_cooldown?,
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The no_cooldown parameter is being passed to Install.formula_installers but this method doesn't accept this parameter. The Install.formula_installers method in Library/Homebrew/install.rb needs to be updated to:

  1. Accept no_cooldown as a parameter in its signature
  2. Pass it through to the FormulaInstaller.new constructor

Without this change, the cooldown feature will not work because the parameter will be ignored, and formulae will always install with the default no_cooldown: false behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +31
# Add the Cooldown Constant here
COOLDOWN_SECONDS = 5 * 60 # 5 minutes
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constant COOLDOWN_SECONDS is defined at the module level (line 31) rather than as a class constant. In Ruby, constants defined at the top level of a file (outside any class/module) become top-level constants. This can lead to:

  1. Namespace pollution - the constant is globally accessible as ::COOLDOWN_SECONDS
  2. Potential conflicts with other gems or code that might define the same constant
  3. Inconsistency with typical Homebrew patterns

Consider moving this inside the FormulaInstaller class:

class FormulaInstaller
  include FormulaCellarChecks
  
  COOLDOWN_SECONDS = 5 * 60 # 5 minutes

This would make it accessible as FormulaInstaller::COOLDOWN_SECONDS and follow Ruby/Homebrew conventions better.

Copilot uses AI. Check for mistakes.
opoo "Pausing installation for #{wait_time}s to mitigate supply chain risk. Use --no-cooldown to skip."

# This is the actual pause
sleep(wait_time)
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The cooldown check uses a blocking sleep() call which will freeze the entire installation process. This has several implications:

  1. User Experience: Users see no progress indication during the wait period - the terminal appears frozen
  2. Interruptibility: Users may think the process is hung and try to kill it
  3. Testing: Makes testing difficult as tests need to wait real time
  4. Batch Operations: When installing multiple formulae, each cooldown is sequential, compounding delays

Consider:

  • Adding a progress indicator (countdown timer) during the sleep
  • Display remaining time with updates (e.g., "Waiting 287 seconds remaining...")
  • For testing, allow the sleep duration to be configurable via an environment variable
  • Consider if the check should happen earlier (during download/fetch) to parallelize the wait

Copilot uses AI. Check for mistakes.
Comment on lines +553 to +579
# --- START DEPENDENCY COOLDOWN LOGIC (INJECTED) ---
if no_cooldown
opoo "Skipping dependency cooldown check due to user request."
else
begin
# This requires Formula#git_commit_time to be implemented in formula.rb
published_time = formula.git_commit_time

if published_time.is_a?(Time)
time_since_publish = Time.now - published_time

if time_since_publish < COOLDOWN_SECONDS
wait_time = (COOLDOWN_SECONDS - time_since_publish).to_i

opoo "Formula '#{formula.name}' was recently published (#{time_since_publish.to_i}s ago)."
opoo "Pausing installation for #{wait_time}s to mitigate supply chain risk. Use --no-cooldown to skip."

# This is the actual pause
sleep(wait_time)
end
end
rescue StandardError => e
# Silently ignore errors during Git time retrieval
odebug "Cooldown check failed: #{e}", Utils::Backtrace.clean(e) if debug?
end
end
# --- END DEPENDENCY COOLDOWN LOGIC (INJECTED) ---
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Inconsistent spacing in comments. The comment on line 558 has a period after "INJECTED" while the end comment on line 579 doesn't:

  • Line 553: # --- START DEPENDENCY COOLDOWN LOGIC (INJECTED) ---
  • Line 579: # --- END DEPENDENCY COOLDOWN LOGIC (INJECTED) ---

While this is minor, typical comment block markers either both have periods or neither do. Consider removing the period from line 553 or adding it to line 579 for consistency.

Copilot uses AI. Check for mistakes.
opoo "Skipping dependency cooldown check due to user request."
else
begin
# This requires Formula#git_commit_time to be implemented in formula.rb
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment references Formula#git_commit_time needing to be "implemented in formula.rb", but the method is already being added in this PR (lines 697-711 of formula.rb). This comment suggests the method doesn't exist yet, which could be confusing to future readers.

Consider updating this comment to:

# Retrieve the formula's Git commit time to determine if cooldown applies
Suggested change
# This requires Formula#git_commit_time to be implemented in formula.rb
# Retrieve the formula's Git commit time to determine if cooldown applies

Copilot uses AI. Check for mistakes.
time_since_publish = Time.now - published_time

if time_since_publish < COOLDOWN_SECONDS
wait_time = (COOLDOWN_SECONDS - time_since_publish).to_i
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential integer overflow when calculating wait_time. On line 565, if time_since_publish is very close to (but less than) COOLDOWN_SECONDS, the subtraction (COOLDOWN_SECONDS - time_since_publish) could result in a float value that when converted with .to_i becomes 0, causing no actual wait.

For example:

  • If time_since_publish = 299.9 and COOLDOWN_SECONDS = 300
  • Then wait_time = (300 - 299.9).to_i = 0.1.to_i = 0
  • No sleep occurs, bypassing the intended protection

Consider using ceil instead:

wait_time = (COOLDOWN_SECONDS - time_since_publish).ceil

This ensures at least 1 second of wait when the cooldown period hasn't fully elapsed.

Suggested change
wait_time = (COOLDOWN_SECONDS - time_since_publish).to_i
wait_time = (COOLDOWN_SECONDS - time_since_publish).ceil

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +51
# ... (around line 20-30 in formula.rb)
require "utils/shell"
require "utils/git" # <-- ADD THIS LINE
require "utils/git_repository"
require "build_environment"
# ...
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect comment placement. The comment # ... (around line 20-30 in formula.rb) on line 46 is misleading since this is actually at line 46, not 20-30. Additionally, lines 47-51 show require statements that don't align with the actual file structure.

The comment suggests adding require "utils/git" but require "utils/shell", require "utils/git_repository", and require "build_environment" are already present in the diff. This is confusing. Either:

  1. Remove the misleading comment and keep only the necessary require statement
  2. Update the comment to accurately reflect what's being added
Suggested change
# ... (around line 20-30 in formula.rb)
require "utils/shell"
require "utils/git" # <-- ADD THIS LINE
require "utils/git_repository"
require "build_environment"
# ...
require "utils/git"

Copilot uses AI. Check for mistakes.
Comment on lines +553 to +579
# --- START DEPENDENCY COOLDOWN LOGIC (INJECTED) ---
if no_cooldown
opoo "Skipping dependency cooldown check due to user request."
else
begin
# This requires Formula#git_commit_time to be implemented in formula.rb
published_time = formula.git_commit_time

if published_time.is_a?(Time)
time_since_publish = Time.now - published_time

if time_since_publish < COOLDOWN_SECONDS
wait_time = (COOLDOWN_SECONDS - time_since_publish).to_i

opoo "Formula '#{formula.name}' was recently published (#{time_since_publish.to_i}s ago)."
opoo "Pausing installation for #{wait_time}s to mitigate supply chain risk. Use --no-cooldown to skip."

# This is the actual pause
sleep(wait_time)
end
end
rescue StandardError => e
# Silently ignore errors during Git time retrieval
odebug "Cooldown check failed: #{e}", Utils::Backtrace.clean(e) if debug?
end
end
# --- END DEPENDENCY COOLDOWN LOGIC (INJECTED) ---
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Security consideration: The cooldown mechanism uses Git commit time which can be manipulated by attackers who have already compromised a repository. An attacker with write access to a tap could:

  1. Backdate Git commits using --date flag to bypass the cooldown
  2. Rebase/rewrite history to make malicious formulae appear older

While this doesn't defeat the purpose entirely (attackers with repo access are already in a privileged position), consider documenting this limitation and/or:

  • Also checking the GitHub API for package publication time as a secondary validation
  • Warning users when Git timestamps are suspiciously old but the formula is new to the tap
  • Logging/alerting when commit timestamps significantly differ from current time during cooldown checks

Copilot uses AI. Check for mistakes.
force:                      force?,
debug:                      debug?,
quiet:                      quiet?,
verbose:                    verbose?,
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing no_cooldown parameter in fetch_dependency. The fetch_dependency method creates a FormulaInstaller instance (lines 886-903) but doesn't pass the no_cooldown parameter, while install_dependency does (line 963).

This means:

  1. Dependencies will always use the default no_cooldown: false during the fetch phase
  2. Inconsistency between fetch and install behavior for dependencies

Add no_cooldown: @no_cooldown, to the FormulaInstaller.new call in fetch_dependency to ensure consistent behavior.

Suggested change
verbose:                    verbose?,
verbose:                    verbose?,
no_cooldown:                @no_cooldown,

Copilot uses AI. Check for mistakes.
sig { params(dependent: Formula, inherited_options: Options).returns(BuildOptions) }
def effective_build_options_for(dependent, inherited_options = Options.new)
args = dependent.build.used_options
args  = dependent.build.used_options
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to   is useless, since its value is never read.

Copilot uses AI. Check for mistakes.
@botantony
Copy link
Member

I don't think this is needed as packages are added to the API after some short delay

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants