Skip to content

fix(core): recover from SIGKILL race in unlock_shrreg() - #260

Closed
Princess0407 wants to merge 1 commit into
Project-HAMi:mainfrom
Princess0407:fix-unlock-shrreg-sigkill-race
Closed

fix(core): recover from SIGKILL race in unlock_shrreg()#260
Princess0407 wants to merge 1 commit into
Project-HAMi:mainfrom
Princess0407:fix-unlock-shrreg-sigkill-race

Conversation

@Princess0407

@Princess0407 Princess0407 commented Aug 9, 2026

Copy link
Copy Markdown

Description

unlock_shrreg() has a microsecond race window between owner_pid = 0 and sem_post(). If the kernel delivers SIGKILL in that window, the semaphore stays locked forever but owner_pid is already cleared. lock_shrreg() recovery only handled current_owner != 0 (dead owner cleanup), so all waiting processes hung indefinitely.

This PR adds recovery for the current_owner == 0 case after repeated timeouts, forcing a sem_post() to unstick the semaphore. It also replaces the vague // TODO: irregular exit here will hang pending locks with a proper comment.

Changes

  • lock_shrreg(): add current_owner == 0 && trials > 5 recovery branch
  • unlock_shrreg(): replace vague TODO with descriptive comment

Why not swap sem_post() and owner_pid = 0?

If sem_post() runs first, a new acquirer can set owner_pid before the original unlocker clears it overwriting the legitimate owner. Recovery in the waiter is the safe design.

Related

  • Project-HAMi/HAMi #2500 : TODO issue in main repo
  • Project-HAMi/HAMi #2125

Summary by CodeRabbit

  • Bug Fixes
    • Improved recovery from rare shared-memory locking races.
    • Prevented repeated timeouts when a stale lock is detected.
    • Improved reliability when acquiring and releasing shared resources.
    • Added safeguards to restore normal operation after interrupted processes.
    • Improved error reporting and recovery handling for lock-related failures.

@hami-robot

hami-robot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: Princess0407
Once this PR has been reviewed and has the lgtm label, please assign archlitchi for approval. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@hami-robot hami-robot Bot added the size/S label Aug 9, 2026
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The shared-region semaphore lock now recovers when a process clears owner_pid but terminates before posting the semaphore. The unlock path documents this race and points to the recovery in lock_shrreg().

Changes

Semaphore recovery

Layer / File(s) Summary
Ownerless timeout recovery and race documentation
src/multiprocess/multiprocess_memory_limit.c
After repeated timeouts with no owner, lock_shrreg() force-posts the semaphore when its value is negative, logs the result, waits briefly, and retries acquisition. unlock_shrreg() documents the SIGKILL race that can leave the semaphore locked.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

  • Project-HAMi#2500 — Describes the same lock_shrreg()/unlock_shrreg() semaphore recovery race.

Suggested labels: enhancement

Suggested reviewers: chaunceyjiang, archlitchi

Poem

A rabbit found the semaphore tight,
With no owner left in sight.
It posted once, then tried anew,
And marked the SIGKILL race too.
Hop, hop—the lock works right!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: recovery from the SIGKILL race in unlock_shrreg().
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

@coderabbitai coderabbitai Bot added the enhancement New feature or request label Aug 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/multiprocess/multiprocess_memory_limit.c`:
- Around line 790-800: The ownerless semaphore recovery branch in the timeout
loop must be made cross-process exclusive and bounded. Around the current_owner
== 0 recovery in the relevant wait function, add a shared recovery claim so only
one waiter can perform the forced sem_post, check and handle sem_post failure,
and track/limit recovery attempts so they cannot continue indefinitely before
the existing trials > 30 failure path; preserve the existing retry behavior when
recovery is successfully claimed.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 08d39a26-b0b8-444d-bf70-da2a6dac5b78

📥 Commits

Reviewing files that changed from the base of the PR and between 5496322 and 44f5912.

📒 Files selected for processing (1)
  • src/multiprocess/multiprocess_memory_limit.c

Comment on lines +790 to +800
// RECOVERY: unlock_shrreg() race where owner_pid was cleared
// but sem_post() never executed
// if owner_pid == 0 and we are still timing out, the semaphore
// is stuck locked with no owner. Force a post to unstick it.
if (current_owner == 0 && trials > 5) {
LOG_WARN("Detected stuck semaphore with owner_pid == 0 "
"(possible unlock race), forcing recovery post");
sem_post(&region->sem);
usleep(10000); // 10ms for semaphore to propagate
continue;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 12 \
  'sem_init|pshared|SEM_WAIT_TIME|SEM_WAIT_RETRY_TIMES|sem_timedwait|sem_post|owner_pid|lock_shrreg|unlock_shrreg' \
  src/multiprocess

Repository: Project-HAMi/HAMi-core

Length of output: 43127


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

src = Path("src/multiprocess/multiprocess_memory_limit.c").read_text()
m = re.search(r"void\s+lock_shrreg\(\)\s*\{(?P<body>.*?)\n\}", src, re.S)
if not m:
    raise SystemExit("lock_shrreg not found")
body = m.group("body")
print("lock_shrreg sem_timedwait_count:", body.count("sem_timedwait(&region->sem"))
print("lock_shrreg owner_pid_set_count:", body.count("region->owner_pid = region_info.pid"))
print("lock_shrreg recovery_post_count:", len(re.findall(r"sem_post\(&region->sem\);", body)))
print("lock_shrreg continuation_count:", body.count("continue;"))

checks = {
    "sem_timedwait_before_owner_publish": "sem_timedwait(&region->sem" in body and "region->owner_pid = region_info.pid" in body,
    "recovery_executes_after_timedwait": "etimedout" in body.lower() or "ETIMEDOUT" in body,
    "recovery_continues": "sem_post(&region->sem)" in body and body.find("sem_post(&region->sem)") > body.find("current_owner == 0 && trials > 5"),
}
for k, v in checks.items():
    print(f"{k}: {v}")

init = Path("src/multiprocess/multiprocess_shrreg.c").read_text() if Path("src/multiprocess/multiprocess_shrreg.c").exists() else ""
if init:
    print("init sem_init_count:", len(re.findall(r"sem_init\(&region->sem,\s*1,\s*1\)", init)))
    print("owner_pid_zero_init_nearby:", re.search(r"owner_pid\s*=\s*0", init))
else:
    init = src
    print("no init file; sem_init occurrences in memory_limit.c:", len(re.findall(r"sem_init\(&region->sem,\s*1,\s*1\)", src)))
PY

sed -n '1090,1170p' src/multiprocess/multiprocess_memory_limit.c

Repository: Project-HAMi/HAMi-core

Length of output: 3926


Make ownerless semaphore recovery owned and bounded.

current_owner == 0 only shows that no owner was published after sem_timedwait() timed out. Every timeout after trial 5 can take this branch, and multiple waiters can each execute sem_post(&region->sem). Add a cross-process recovery claim around the post, check the return value of sem_post(), and limit the recovery attempts before the existing trials > 30 failure path.

🧰 Tools
🪛 Cppcheck (2.21.0)

[warning] 795-795: If resource allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfResources)

🤖 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/multiprocess/multiprocess_memory_limit.c` around lines 790 - 800, The
ownerless semaphore recovery branch in the timeout loop must be made
cross-process exclusive and bounded. Around the current_owner == 0 recovery in
the relevant wait function, add a shared recovery claim so only one waiter can
perform the forced sem_post, check and handle sem_post failure, and track/limit
recovery attempts so they cannot continue indefinitely before the existing
trials > 30 failure path; preserve the existing retry behavior when recovery is
successfully claimed.

When a process is SIGKILL'd between clearing owner_pid and posting
the semaphore in unlock_shrreg(), the semaphore remains locked with
no owner. lock_shrreg()'s recovery logic only handled dead owners
(current_owner != 0), leaving the lock stuck forever when
current_owner == 0.
This change adds a recovery branch in lock_shrreg() that detects
owner_pid == 0 after repeated timeouts and forces a sem_post()
to unstick pending waiters. Also replaces the vague TODO with a
descriptive comment explaining the race and recovery path.

Related: #2500, #2125, solves #2500
Signed-off-by: Priyanka Tiwari <priyankatiwari140419@gmail.com>
@Princess0407
Princess0407 force-pushed the fix-unlock-shrreg-sigkill-race branch from 44f5912 to 1b826d7 Compare August 9, 2026 10:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/multiprocess/multiprocess_memory_limit.c`:
- Around line 792-794: Replace the sem_getvalue()/sem_val < 0 heuristic in the
current_owner recovery path with a shared atomic recovery claim/state. Ensure
only one process can claim recovery after repeated trials, including when
owner_pid is cleared but sem_post was interrupted, then perform the existing
semaphore recovery without relying on waiter counts.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 75e9d51e-f030-446d-9274-047b074d10a8

📥 Commits

Reviewing files that changed from the base of the PR and between 44f5912 and 1b826d7.

📒 Files selected for processing (1)
  • src/multiprocess/multiprocess_memory_limit.c

Comment on lines +792 to +794
if (current_owner == 0 && trials > 5) {
int sem_val;
if (sem_getvalue(&region->sem, &sem_val) == 0 && sem_val < 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -i 'linux|ubuntu|debian|alpine|glibc|musl' \
  --glob 'Dockerfile*' --glob '*.yml' --glob '*.yaml' --glob '*.md' . || true

if command -v man >/dev/null 2>&1; then
  MANPAGER=cat man 3 sem_getvalue | grep -E 'zero|negative|Linux adopts|changed'
else
  echo "Verify sem_getvalue behavior in the target build image."
fi

Repository: Project-HAMi/HAMi-core

Length of output: 216


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '## Candidate files\n'
git ls-files | rg 'Dockerfile|\.ya?ml|README\.md|src/multiprocess/multiprocess_memory_limit\.c' || true

printf '\n## Target docs references\n'
rg -n -i 'multiprocess|HAMi|gpu|linux|ubuntu|debian|alpine|glibc|musl|docker|container' \
  --glob 'Dockerfile*' --glob '*.yml' --glob '*.yaml' --glob '*.md' . || true

printf '\n## Relevant C source excerpts\n'
wc -l src/multiprocess/multiprocess_memory_limit.c
sed -n '720,850p' src/multiprocess/multiprocess_memory_limit.c

printf '\n## sem_getvalue usage and related recovery code\n'
rg -n 'sem_getvalue|sem_post|recover|trials|owner|SIGKILL|multiprocess_memory_limit' src/multiprocess/multiprocess_memory_limit.c

Repository: Project-HAMi/HAMi-core

Length of output: 16655


🌐 Web query:

man sem_getvalue POSIX Linux sem_getvalue returns non-negative value zero number of processes blocked

💡 Result:

Under the POSIX standard, if one or more processes or threads are blocked on a semaphore, the value placed in the sval argument by sem_getvalue may either be 0 or a negative number whose absolute value represents the count of blocked processes or threads [1][2][3]. Linux explicitly adopts the former behavior: when processes or threads are blocked, sem_getvalue returns 0 in the location pointed to by sval [1][4][5]. It does not provide the count of blocked processes as a negative number [1][6]. The function sem_getvalue itself returns 0 on success and -1 on error (with errno set) [1][2][4]. Note that any value retrieved by sem_getvalue may already be outdated by the time the application receives it, as the state of the semaphore can change immediately after the call [1][5][3].

Citations:


Use a shared recovery claim instead of sem_val < 0.

sem_getvalue() does not reliably indicate blocked waiters. POSIX allows 0 for blocked waiters, and Linux reports 0 rather than a negative count. If SIGKILL interrupts unlock_shrreg() after clearing owner_pid but before sem_post(), this condition leaves the semaphore locked, so waiters keep timing out instead of breaking out. Track recovery with shared atomic state/state rather than inferring the stuck lock from sem_getvalue().

🤖 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/multiprocess/multiprocess_memory_limit.c` around lines 792 - 794, Replace
the sem_getvalue()/sem_val < 0 heuristic in the current_owner recovery path with
a shared atomic recovery claim/state. Ensure only one process can claim recovery
after repeated trials, including when owner_pid is cleared but sem_post was
interrupted, then perform the existing semaphore recovery without relying on
waiter counts.

@mesutoezdil

Copy link
Copy Markdown
Contributor

This is being closed because it does not comply with the contribution guidelines.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants