sysupgrade: stop the ramfs pivot leaving debris in the overlay - #2280
Conversation
Cameras upgraded since the ramfs pivot landed come back carrying two files
that nothing put there on purpose, reported on four SoCs at once:
/overlay/root/dev/null
/overlay/root/ram/
Both are enter_ramfs leaking into the root it is leaving.
/dev/null is the interesting one. A shell redirection CREATES its target, and
enter_ramfs moves /dev — the devtmpfs that supplied the node — out of the old
root and then keeps running there for four more commands. So every `2>/dev/null`
in that window discards nothing: it writes a regular file named `null` into a
directory the shipped rootfs leaves empty. That root is an overlay, so the file
lands in the upper layer and outlives the upgrade, shadowing the character
device on every subsequent boot until /init moves devtmpfs back over it.
Confirmed on an hi3516av300, and the mode is the fingerprint: root's umask is
0077 and the artifact is 0600, exactly what open(O_CREAT, 0666) yields. Holding
a descriptor open on the real node instead survives the mount move, the pivot
and the exec, because a descriptor names the inode rather than the path — the
same window then leaves nothing behind. Reproduced both ways on hardware with no
flash writes at all: old spelling creates the file, `2>&3` does not.
/ram is the mount point itself. The rootfs ships no /ram, so `mkdir -p` writes
it into the upper layer, and nothing ever removed it — not after a successful
pivot, not after a failed one, and not after the early bail-outs that leave a
tmpfs mounted on it too. Reclaim it on each: after the pivot the old root is at
/mnt with the tmpfs already detached, and the single call site is the one place
that means "no pivot happened". rmdir, never rm -rf: if the tmpfs is somehow
still attached, refusing is the right answer.
Neither artifact is dangerous today — devtmpfs masks the file early enough that
nothing has been observed writing through it — but it is a silent write to
flash on every upgrade, and a trap for anyone who later adds a redirection to
/init between its own pivot and its /dev move.
scr_version bumped so self_update actually delivers this to a running camera:
it compares versions, so a fix that reuses 1.0.57 would never install.
Verified end to end on the lab hi3516av300: 2.6.08.15 -> 2.6.08.16 with the
patched script, clean reboot, /overlay/root/dev empty and /overlay/root/ram
absent afterwards. Tests cover both halves, and fail when either is reverted.
Reported-by: usa- <https://github.com/usa->
Ref: OpenIPC/majestic-webui#120
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR Summary by Qodosysupgrade: prevent ramfs pivot from writing /dev/null and /ram into overlay
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
1. Ramfs cleanup may fail
|
Raised in review. RAM_ROOT is overridable from the environment, and two places downstream quietly assume it starts with a slash: /proc/mounts records mount points absolutely, so the "is this tmpfs one of ours" check in ramfs_discard could never match a relative path; and the second phase reaches the same directory through the old root as "/mnt$RAM_ROOT", which for `ram` composes to "/mntram" and removes nothing. Neither is reachable today — the default is /ram and the test harness passes an absolute sandbox path — but both fail silently rather than loudly, which is the kind of thing that survives a refactor. Normalise once at the definition instead of teaching every use site to cope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
/review |
| # /mnt to take it from: the first flashcp write is a few lines away and the | ||
| # old root does not survive it (OpenIPC/majestic-webui#120). | ||
| rmdir "/mnt$RAM_ROOT" 2>/dev/null |
There was a problem hiding this comment.
1. Ramfs cleanup may fail 🐞 Bug ≡ Correctness
The ramfs phase now runs rmdir "/mnt$RAM_ROOT", but if the script takes the existing “busybox --list is a build option” fallback path, it does not create a rmdir applet symlink, so this cleanup can fail silently and leave the mount-point debris behind.
Agent Prompt
### Issue description
The ramfs (second-phase) cleanup uses `rmdir "/mnt$RAM_ROOT"`, but in the code path where BusyBox `--list` is not available, the staged ramfs only gets symlinks for a fixed list of applets that currently omits `rmdir`. In that fallback scenario, `rmdir` may be `not found` (and the error is redirected away), so the old-root mount-point directory may not be reclaimed.
### Issue Context
`enter_ramfs()` explicitly supports a BusyBox configuration where `busybox --list` is unavailable and falls back to a hardcoded applet list; the new cleanup depends on `rmdir` being available in that staged `/bin`.
### Fix Focus Areas
- general/overlay/usr/sbin/sysupgrade[622-627]
- general/overlay/usr/sbin/sysupgrade[1078-1086]
### Suggested fix
Either:
1) Add `rmdir` to the fallback applet list that gets symlinked into `$RAM_ROOT/bin`, **or**
2) Call it via BusyBox directly in phase 2 (e.g. `/bin/busybox rmdir "/mnt$RAM_ROOT" ...`) so it does not rely on a symlinked applet name.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Code review by qodo was updated up to the latest commit 874036c |
Caught in review, and a real hole in the previous commit. The mount-point reclaim it added runs AFTER the pivot, where the staged applet symlinks are the only tools in existence — and `rmdir` was not among them. `rm` is on the fallback list; `rmdir` never was, because until now nothing after the pivot needed it. `busybox --list` populates the symlinks when it is available, which is the path every stock OpenIPC build takes (282 applets on the lab av300, rmdir included), so this would not have bitten there. But --list is a build option, and the hardcoded list is precisely the "camera without it" case — where the cleanup would have failed as "not found", swallowed by its own 2>/dev/null, and left the debris it exists to remove. Silently doing nothing is the worst shape for this bug, since the symptom is identical to not having the fix at all. The test now derives the requirement from the fallback list rather than trusting it, and fails if rmdir leaves it again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Good catch on both — fixed in
Worth noting for scope: Both now have test invariants, and both fail the suite when reverted: |
|
/review |
|
Code review by qodo was updated up to the latest commit e54aa69 |
|
The remaining At the exact sha and the exact lines the finding cites ( # --list is a build option; fall back to what the flash phase actually uses.
for applet in sh ash awk basename cat cut dd flash_eraseall flashcp grep \
head ln losetup ls mkdir mount od printf reboot rm rmdir sed sleep \
sync tail timeout umount xxd; do
Guarded by a test either way, so a regression would be caught rather than argued about: |
Reported by @usa- in OpenIPC/majestic-webui#120: cameras upgraded since the ramfs pivot landed come back carrying two entries nothing put there on purpose, on four SoCs at once.
His guess in the report was right — "something is writing to
/dev/null(like2>/dev/null) while it has not yet been created or is no longer available."/dev/nullA shell redirection creates its target.
enter_ramfsmoves/dev— the devtmpfs that supplied the node — out of the old root, and then keeps running there for four more commands. Every2>/dev/nullin that window therefore discards nothing; it writes a regular file namednullinto a directory the shipped rootfs leaves empty. That root is an overlay, so the file lands in the upper layer and outlives the upgrade.The mode is the fingerprint. Root's umask on the camera is
0077, and the artifact is0600— exactly whatopen(O_CREAT, 0666)yields:/rom/devships nonullnode at all, so there is nothing for overlayfs to have copied up — the file is new.Holding a descriptor open on the real node survives the mount move, the pivot and the exec, because a descriptor names the inode rather than the path. Reproduced both ways on real hardware with no flash writes, by moving
/devaside and putting it back:2>/dev/null(old)-rw-------regular file2>&3(new)/ramThe mount point itself. The rootfs ships no
/ram, somkdir -p "$RAM_ROOT"writes it into the upper layer, and nothing ever removed it — not after a successful pivot, not after a failed one, and not after the early bail-outs, which leave a tmpfs mounted on it as well.Reclaimed on each path now. After the pivot the old root sits at
/mntwith the tmpfs already detached, so the second phase takes it back before the firstflashcpwrite destroys the filesystem it lives on; the singleenter_ramfscall site covers every bail-out.rmdir, neverrm -rf— if the tmpfs is somehow still attached, refusing is the right answer.Impact
Neither artifact is dangerous today:
/initmoves devtmpfs back over/devearly enough that nothing has been observed writing through the file. But it is a silent write to flash on every upgrade, and a trap for anyone who later adds a redirection to/initbetween its ownpivot_rootand itsmount -o move /rom/dev /dev.Verification
End to end on the lab hi3516av300, with the patched script and the artifacts cleared first:
test_sysupgrade.shgains one behavioural check (a failed pivot reclaims its mount point) and seven source invariants covering the redirect window,ramfs_unwind, where fd 3 is opened, andrmdir-not-rm -rf. Both halves fail the suite when reverted:scr_versionis bumped to 1.0.58 soself_updateactually delivers this — it compares versions, so reusing 1.0.57 would never install on a running camera.🤖 Generated with Claude Code