Skip to content

Port build hook timeout commits from upstream - #583

Open
lisanna-dettwyler wants to merge 3 commits into
DeterminateSystems:mainfrom
lisanna-dettwyler:detnix-build-hook-timeout
Open

Port build hook timeout commits from upstream#583
lisanna-dettwyler wants to merge 3 commits into
DeterminateSystems:mainfrom
lisanna-dettwyler:detnix-build-hook-timeout

Conversation

@lisanna-dettwyler

@lisanna-dettwyler lisanna-dettwyler commented Jul 30, 2026

Copy link
Copy Markdown

Motivation

Ports recent build-hook kill timeout changes from upstream, needed for our custom slurm build hook

Context

NixOS#14760
NixOS#16230

Summary by CodeRabbit

  • New Features

    • Added configurable timeouts for build hooks, allowing them to shut down gracefully before being forcefully terminated.
    • Added the build-hook-kill-timeout setting, defaulting to 500 milliseconds.
  • Bug Fixes

    • Improved build-remote signal handling during startup.
    • Skipped an environment-incompatible filesystem test to prevent false failures.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@lisanna-dettwyler, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a77acb7c-dd10-4b50-a35d-63db0cf0e5c0

📥 Commits

Reviewing files that changed from the base of the PR and between d7cf5ab and 89b7008.

📒 Files selected for processing (1)
  • src/libutil-tests/file-system-at.cc
📝 Walkthrough

Walkthrough

The change adds a configurable build-hook termination timeout. Hooks receive SIGTERM before SIGKILL. Pid manages delayed killing in a background thread. build-remote restores and unblocks SIGTERM. The readLinkAt test skips in EC2 container chroot stores.

Changes

Build hook termination

Layer / File(s) Summary
Pid kill timeout mechanism
src/libutil/include/nix/util/processes.hh, src/libutil/unix/processes.cc
Pid stores a kill timeout, starts delayed SIGKILL handling after non-SIGKILL termination, and joins the timeout thread before returning.
Build hook timeout configuration
src/libstore/include/nix/store/worker-settings.hh, src/libstore/unix/include/nix/store/build/hook-instance.hh, src/libstore/unix/build/hook-instance.cc, src/libstore/build/derivation-building-goal.cc
WorkerSettings adds build-hook-kill-timeout. HookInstance accepts the duration, configures SIGTERM and the timeout, and receives the setting during construction.
Build-remote SIGTERM initialization
src/nix/build-remote/build-remote.cc
build-remote restores the default SIGTERM handler and unblocks SIGTERM before logger initialization.

Filesystem test environment

Layer / File(s) Summary
readLinkAt environment guard
src/libutil-tests/file-system-at.cc
The readLinkAt test skips when it runs in an EC2 container chroot store.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WorkerSettings
  participant HookInstance
  participant Pid
  participant BuildHookProcess
  WorkerSettings->>HookInstance: provide buildHookKillTimeout
  HookInstance->>Pid: set kill signal to SIGTERM
  HookInstance->>Pid: setKillTimeout(timeout)
  HookInstance->>Pid: kill(SIGTERM)
  Pid->>BuildHookProcess: send SIGTERM
  Pid->>Pid: start timeout thread
  Pid->>BuildHookProcess: send SIGKILL after timeout
Loading

Suggested reviewers: edolstra

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies that the pull request ports upstream build-hook timeout changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lisanna-dettwyler lisanna-dettwyler changed the title Detnix build hook timeout Port build hook timeout commits from upstream Jul 30, 2026
@lisanna-dettwyler
lisanna-dettwyler force-pushed the detnix-build-hook-timeout branch from 22a0b7c to f58683d Compare July 31, 2026 01:30
@lisanna-dettwyler
lisanna-dettwyler changed the base branch from master to main July 31, 2026 01:30
lisanna-dettwyler and others added 2 commits July 31, 2026 10:47
This gives custom build hooks the chance to perform any needed cleanup,
which is impossible when killed with SIGKILL. After 500ms, if the hook
still hasn't exited, it will be forcibly killed with SIGKILL.

Resolves NixOS#14760

Signed-off-by: Lisanna Dettwyler <lisanna.dettwyler@gmail.com>
Signed-off-by: Lisanna Dettwyler <lisanna.dettwyler@saronic.com>
@lisanna-dettwyler
lisanna-dettwyler force-pushed the detnix-build-hook-timeout branch from f58683d to d7cf5ab Compare July 31, 2026 14:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/libutil/unix/processes.cc (1)

83-115: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Make the timeout worker independent of mutable Pid state and always join it.

wait() sets pid to -1. If the timeout worker runs before Line 112 sets killed, Line 94 can send SIGKILL to -1 or to PID 1, instead of the build hook.

If wait() throws, Lines 111-113 do not run. The joinable killThread then remains in the object and can cause std::terminate.

Capture immutable target values before starting the worker. Use a scope guard that cancels and joins the worker on both normal and exceptional exits. Coordinate completion before wait() clears the target PID.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libutil/unix/processes.cc` around lines 83 - 115, The timeout worker
lambda captures pid by reference, but wait() modifies pid to -1, causing the
worker to send SIGKILL to an incorrect target if it executes after wait().
Additionally, if wait() throws an exception, the killThread cleanup code at
lines 111-113 never runs, leaving an unjoinable thread that causes
std::terminate. Capture an immutable copy of the target PID before creating the
killThread lambda so the worker uses the stable value, and implement a scope
guard that ensures killThread is always joined on both normal and exceptional
exits before wait() is called, preserving state consistency and eliminating the
exception-safety issue.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/libstore/include/nix/store/worker-settings.hh`:
- Around line 137-141: The buildHookKillTimeout setting currently accepts zero,
which causes Pid::kill() to skip the SIGKILL timeout enforcement and allows a
hook ignoring SIGTERM to block shutdown indefinitely. Add a validator to the
buildHookKillTimeout Setting that rejects zero values, ensuring only positive
timeout values are accepted and the documented SIGKILL behavior is preserved.

In `@src/libutil-tests/file-system-at.cc`:
- Line 16: Update the readLinkAt.works test so the GTEST_SKIP call is
conditional on detecting EC2 container chroot stores, while preserving the
existing _WIN32 skip and allowing assertions to run on other platforms.

In `@src/libutil/include/nix/util/processes.hh`:
- Around line 39-40: Update Pid::Pid(Pid &&) to preserve killTimeout and move
killThread from the source object, alongside the existing moved fields. Update
Pid::swap to exchange both killTimeout and killThread so move assignment retains
the complete timeout state.

---

Outside diff comments:
In `@src/libutil/unix/processes.cc`:
- Around line 83-115: The timeout worker lambda captures pid by reference, but
wait() modifies pid to -1, causing the worker to send SIGKILL to an incorrect
target if it executes after wait(). Additionally, if wait() throws an exception,
the killThread cleanup code at lines 111-113 never runs, leaving an unjoinable
thread that causes std::terminate. Capture an immutable copy of the target PID
before creating the killThread lambda so the worker uses the stable value, and
implement a scope guard that ensures killThread is always joined on both normal
and exceptional exits before wait() is called, preserving state consistency and
eliminating the exception-safety issue.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a8784a0d-0e01-430d-98bb-c0f716962803

📥 Commits

Reviewing files that changed from the base of the PR and between 0ff4663 and d7cf5ab.

📒 Files selected for processing (8)
  • src/libstore/build/derivation-building-goal.cc
  • src/libstore/include/nix/store/worker-settings.hh
  • src/libstore/unix/build/hook-instance.cc
  • src/libstore/unix/include/nix/store/build/hook-instance.hh
  • src/libutil-tests/file-system-at.cc
  • src/libutil/include/nix/util/processes.hh
  • src/libutil/unix/processes.cc
  • src/nix/build-remote/build-remote.cc

Comment on lines +137 to +141
Setting<uint32_t> buildHookKillTimeout{
this,
500,
"build-hook-kill-timeout",
"How long to wait in milliseconds for build hooks to exit on interrupt before sending SIGKILL."};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject a zero build-hook kill timeout.

This setting accepts 0. Pid::kill() then skips the SIGKILL timeout worker, so a hook that ignores SIGTERM can block shutdown indefinitely. This conflicts with the documented SIGKILL behavior.

Reject zero during setting validation, or explicitly document that zero disables forced termination.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libstore/include/nix/store/worker-settings.hh` around lines 137 - 141,
The buildHookKillTimeout setting currently accepts zero, which causes
Pid::kill() to skip the SIGKILL timeout enforcement and allows a hook ignoring
SIGTERM to block shutdown indefinitely. Add a validator to the
buildHookKillTimeout Setting that rejects zero values, ensuring only positive
timeout values are accepted and the documented SIGKILL behavior is preserved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@lisanna-dettwyler This seems like a relevant comment. The ability to disable the timeout seems useful, but it should be documented.

Comment thread src/libutil-tests/file-system-at.cc Outdated

TEST(readLinkAt, works)
{
GTEST_SKIP() << "Broken on EC2 container chroot stores";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'EC2|ec2|chroot|CHROOT|store.*chroot|chroot.*store|GTEST_SKIP' src maintainers

Repository: DeterminateSystems/nix-src

Length of output: 50383


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- file-system-at.cc ---\n'
cat -n src/libutil-tests/file-system-at.cc | sed -n '1,90p'

printf '\n--- build/test config references to file-system-at target/list ---\n'
rg -n 'file-system-at|fileSystemAt|readLinkAt|GTEST_SKIP|WIN32|Windows' meson.build meson_options.txt src/libutil-tests.cmake src/libutil-tests/CMakeLists.txt . 2>/dev/null | head -n 120

Repository: DeterminateSystems/nix-src

Length of output: 255


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- file-system-at.cc ---'
cat -n src/libutil-tests/file-system-at.cc | sed -n '1,90p'

echo
echo '--- build/test config references ---'
rg -n -C 3 'file-system-at|fileSystemAt|readLinkAt|GTEST_SKIP|WIN32|Windows' meson.build meson_options.txt src/libutil-tests.cmake src/libutil-tests/CMakeLists.txt 2>/dev/null || true

Repository: DeterminateSystems/nix-src

Length of output: 4589


Scope the skip to EC2 container chroot stores.

GTEST_SKIP() at the start of TEST(readLinkAt, works) runs before all assertions and makes the #ifdef _WIN32 skip unreachable. Wrap the skip in a check for EC2 container chroot stores before removing readLinkAt coverage on other platforms.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libutil-tests/file-system-at.cc` at line 16, Update the readLinkAt.works
test so the GTEST_SKIP call is conditional on detecting EC2 container chroot
stores, while preserving the existing _WIN32 skip and allowing assertions to run
on other platforms.

Comment on lines +39 to +40
std::chrono::milliseconds killTimeout;
std::thread killThread;

@coderabbitai coderabbitai Bot Jul 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve timeout state when moving Pid.

Pid::Pid(Pid &&) copies only pid, separatePG, and killSignal. A moved Pid loses killTimeout. The shown swap also omits killTimeout and killThread, so move assignment loses them too.

Initialize killTimeout and move killThread in the move constructor. Swap both fields in swap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libutil/include/nix/util/processes.hh` around lines 39 - 40, Update
Pid::Pid(Pid &&) to preserve killTimeout and move killThread from the source
object, alongside the existing moved fields. Update Pid::swap to exchange both
killTimeout and killThread so move assignment retains the complete timeout
state.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is probably worth fixing even if it doesn't affect this PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

processes_cc="$(fd -a '^processes\.cc$' src/libutil | head -n1)"
if [ -z "$processes_cc" ]; then
  echo "src/libutil unix processes implementation was not found." >&2
  exit 1
fi

ast-grep outline "$processes_cc" --items all --match 'Pid|kill|wait|setKillTimeout' --view expanded
rg -n -C 12 --glob 'processes.cc' \
  'Pid::Pid\(Pid &&|Pid::operator=\(Pid &&|void Pid::setKillTimeout|int Pid::kill|int Pid::wait|killThread|killTimeout' \
  src/libutil

ast-grep outline src/libutil/include/nix/util/processes.hh --match 'Pid' --view expanded

Length of output: 12393


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Pid destructor and adjacent lifecycle code ---'
sed -n '35,175p' src/libutil/unix/processes.cc

printf '%s\n' '--- Direct Pid moves and move assignments ---'
rg -n -C 5 --glob '*.{cc,hh}' \
  '(?:std::move\([^)]*\)|return)\s*.*\bPid\b|\bPid\s+\w+\s*=\s*std::move|\boperator=\(std::move' \
  src

Length of output: 4244


@edolstra, verified.

The minimal safe fix is:

  • Copy killTimeout in Pid::Pid(Pid &&).
  • Swap killTimeout in swap(Pid &, Pid &).

Do not move or swap killThread. Its lambda accesses the source Pid state and the stack-local killed flag. Pid::kill() joins it on the normal path.

You are interacting with an AI system.

Signed-off-by: Lisanna Dettwyler <lisanna.dettwyler@saronic.com>
@lisanna-dettwyler
lisanna-dettwyler force-pushed the detnix-build-hook-timeout branch from d7cf5ab to 89b7008 Compare July 31, 2026 15:01
@lisanna-dettwyler

lisanna-dettwyler commented Jul 31, 2026

Copy link
Copy Markdown
Author

Tacked on a unit test disable because this is the branch we're working off of for our slurm cluster, and that test fails when built on the slurm cluster because of a bind mounted rootfs.

@edolstra

edolstra commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Can you rebase this on the current main branch?

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.

2 participants