Skip to content

majestic: match the daemon by executable, not by a stale pidfile - #2324

Merged
widgetii merged 1 commit into
masterfrom
majestic-init-drop-pidfile
Aug 29, 2026
Merged

majestic: match the daemon by executable, not by a stale pidfile#2324
widgetii merged 1 commit into
masterfrom
majestic-init-drop-pidfile

Conversation

@widgetii

Copy link
Copy Markdown
Member

Supersedes #2245 and #2250. Both reported the same failure from opposite ends and both diagnoses were close but not quite right, so I reproduced the whole thing on a Hi3516EV300 (busybox 1.36.1) and traced it to one flag.

Root cause: -p

Busybox start-stop-daemon's do_procinit() reads only the pid the pidfile holds and never scans /proc. -x is then just a filter applied to that single pid. So the moment the pidfile is stale, start-stop-daemon cannot see the live daemon at all.

With majestic live as pid 1177 and the pidfile poisoned with a dead pid:

# start-stop-daemon -b -m -S -q -p /var/run/majestic.pid -x majestic -- -s
rc=0                      # started a SECOND majestic, pid 1233
# sleep 4; pidof majestic
1177                      # the duplicate is already dead
# cat /var/run/majestic.pid
1233                      # ...and -m wrote its dead pid into the file

The second instance dies on the busy sensor HAL, but -m has already overwritten the pidfile with its pid. Every later start re-poisons the file, so the state is self-perpetuating.

That single mechanism explains both reports without needing any majestic-internal watchdog:

stop() was worse. It passed -p with no -x, so it signalled whatever process now owned that pid. Pointed at an unrelated live process, it killed that process and left majestic running:

# sleep 600 & echo $! > /var/run/majestic.pid
1260
# start-stop-daemon -K -q -p /var/run/majestic.pid
rc=0
# victim 1260: KILLED     # majestic 1177: still running

Two corrections to #2250's reasoning

  • -x majestic (bare name) is not the problem. Busybox compares /proc/PID/cmdline argv[0] as well as the /proc/PID/exe symlink, and I measured the bare form matching correctly (rc=0). The absolute path is not what fixes detection — dropping -p is.
  • Dropping -p without -a is a silent compat regression. argv[0] becomes /usr/bin/majestic, ps output changes, and bare-name start-stop-daemon -x majestic matchers start returning rc=1. Adding -a majestic keeps argv[0] as majestic while -x /usr/bin/majestic still matches through the exe symlink, so nothing observable changes.

The fix

-x /usr/bin/majestic scans every process, and -a majestic keeps the identity stable:

  • start is idempotent. A second start is refused, reports success, and leaves the running daemon untouched — so the script is safe to call unconditionally from a crond watchdog (* * * * * /etc/init.d/S95majestic start), with no pgrep/pidof pre-check that can itself go stale on a zombie.
  • stop polls until the daemon is genuinely gone (measured: 1 s) instead of sleeping a fixed 4 s, and returns non-zero if it survives, so restart aborts rather than stacking a second instance.
  • reload signals through the same matcher instead of killall.

Nothing in the tree reads /var/run/majestic.pid, so removing it is safe; stop deletes a leftover file so it cannot mislead anyone debugging.

Verification (Hi3516EV300 + IMX335, busybox 1.36.1, kernel 4.9.37)

# Check Result
0 sh -n under busybox ash OK
2 stop kills the daemon and clears the pidfile OK, 1.04 s
3 stop when already stopped OK, rc=0 (not FAIL)
4 start — argv[0] stays majestic ps: majestic -s
5 start while running idempotent, rc=0, pid unchanged
6 stale pidfile + start no duplicate spawned
7 pidfile pointing at an innocent process + stop victim survives
8 restart from stopped OK
9 restart while running exactly one daemon, 4.05 s
10 reload SIGHUP delivered, daemon survives
11 kill -9 + unconditional watchdog tick recovers, RTSP back
12 bad argument usage, rc=1
13 pidof / killall / ps / bare -x majestic all unchanged

Cold boot on the fixed script: exactly one majestic, argv[0] = majestic, no pidfile, :554 and :80 listening, and the /etc/TZ re-read from #2244 still propagates (TZ=GMT0).

Follow-up

Eleven other init scripts carry the same -b -m -S -p / -K -p pattern and therefore the same latent duplicate-spawn and wrong-pid-kill defects: S60crond, S01syslogd, S49ntpd, S50dropbear, S96onvifserver, S89edge, S97baresip, S50mdnsd, S60siproxd, S60precision-time, S90matter. They bite far less often because those daemons rarely die and get restarted. Filed separately rather than widening this PR's blast radius across every board's boot path.

Credit to @zavaruev (#2250) for the pidfile-as-stale-oracle diagnosis and the watchdog-safety framing, and to @phedoreanu (#2245) for spotting the duplicate-daemon symptom and insisting stop verify the daemon is actually gone.

Given -p, busybox start-stop-daemon inspects only the pid that file
holds and never scans /proc, so the pidfile is the sole liveness oracle
-- and it goes stale on its own. Measured on a Hi3516EV300 (busybox
1.36.1) with majestic live as pid 1177 and the pidfile poisoned with a
dead pid:

  start-stop-daemon -b -m -S -q -p $PIDFILE -x majestic -- -s

returned 0 and started a SECOND majestic (pid 1233). The duplicate died
four seconds later on the busy sensor HAL, but -m had already written
1233 into the pidfile, so every later start re-poisoned it. That one
mechanism explains both reported symptoms: a pidfile full of pids that
were never a usable daemon, and two daemons fighting over the encoder
while config edits appear to apply to neither.

stop() was worse. It passed -p with no -x, so it signalled whatever now
owned that pid: pointed at an unrelated live process, it killed that
process and left majestic running.

Drop the pidfile and match on -x /usr/bin/majestic, which scans every
process. -a keeps argv[0] as "majestic", so ps output, pidof, killall
and bare-name matchers all behave exactly as before. start is now
idempotent -- a second start is refused and reports success, leaving the
running daemon untouched -- which makes the script safe to call
unconditionally from a crond watchdog. stop polls until the daemon is
really gone before restart proceeds, and reload signals through the same
matcher instead of killall.

Nothing in the tree reads /var/run/majestic.pid; stop removes a leftover
so an older file cannot mislead.

Verified on a Hi3516EV300: stale pidfile no longer spawns a duplicate,
stop no longer touches an unrelated process, stop/start/restart/reload
each behave across nine cycles, kill -9 followed by an unconditional
start recovers, and a cold boot brings up exactly one daemon with RTSP
and HTTP listening.

Co-authored-by: zavaruev <29773394+zavaruev@users.noreply.github.com>
Co-authored-by: Adrian Fedoreanu <1611001+phedoreanu@users.noreply.github.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Majestic: match the daemon by executable instead of stale pidfiles

🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

AI Description

• Matches Majestic by executable, preventing stale pidfiles from spawning duplicates or killing
 unrelated processes.
• Makes starts idempotent and verifies shutdown before allowing restarts.
• Routes reloads through the same matcher while preserving the existing process identity.
Diagram

graph TD
  INIT["Init script"] --> START["Start action"] --> MATCH["Executable matcher"] --> PROC["Majestic process"]
  INIT --> STOP["Stop action"] --> MATCH
  INIT --> RELOAD["Reload action"] --> MATCH
  STOP --> VERIFY{"Daemon exited?"}
  VERIFY -->|No| WAIT["Poll up to 10s"] --> VERIFY
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Validate and repair the pidfile
  • ➕ Retains conventional pidfile-based lifecycle management.
  • ➕ Could minimize command-line changes.
  • ➖ Keeps a fallible cached PID as the primary process identity.
  • ➖ Requires race-safe validation against PID reuse and executable identity.
2. Adopt a process supervisor
  • ➕ Provides authoritative lifecycle tracking and automatic restart policies.
  • ➕ Eliminates custom polling and pidfile handling.
  • ➖ Substantially widens scope across firmware images and boot integration.
  • ➖ Introduces migration and compatibility risk for a single-daemon fix.

Recommendation: Use executable matching as implemented. It directly removes the stale-PID failure mode with minimal scope, while preserving argv[0]; pidfile repair remains race-prone and a supervisor migration is disproportionate to this PR.

Files changed (1) +30 / -14

Bug fix (1) +30 / -14
S95majesticReplace pidfile lifecycle management with executable matching +30/-14

Replace pidfile lifecycle management with executable matching

• Starts, stops, and reloads Majestic by matching /usr/bin/majestic rather than trusting a pidfile, while preserving argv[0] as majestic. Start becomes idempotent, stop polls for confirmed termination and removes stale pidfiles, and restart aborts if shutdown fails.

general/package/majestic/files/S95majestic

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can group findings by type and pick your Finding display, from Minimal to Full

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@widgetii
widgetii merged commit 5ac8cff into master Aug 29, 2026
102 of 103 checks passed
@widgetii
widgetii deleted the majestic-init-drop-pidfile branch August 29, 2026 08:56
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.

1 participant