Skip to content

feat(factory-reset): implement mode 1 (backup / reformat / restore)#16

Merged
JoergZeidler merged 25 commits into
omnect:mainfrom
JoergZeidler:feat/factory-reset-mode1
Jul 8, 2026
Merged

feat(factory-reset): implement mode 1 (backup / reformat / restore)#16
JoergZeidler merged 25 commits into
omnect:mainfrom
JoergZeidler:feat/factory-reset-mode1

Conversation

@JoergZeidler

@JoergZeidler JoergZeidler commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds BootMode::FactoryReset, detected via a new BootEnvKey::FactoryReset bootloader
    env key (feature factory-reset).
  • Implements the mode 1 reset sequence in mode::factory_reset: build a selective
    preserve-list (config.rs), mount factory(ro)/etc(rw)/data(rw) + overlays, back up
    preserved paths, reformat data and etc (ext4), remount etc(rw)/data(rw) (factory
    excluded per spec), and restore.
  • Adds FactoryResetError, classified as ContinueDegraded -> a failed or unsupported
    reset never blocks boot; outcome is always recorded in the ODS status JSON
    (FactoryResetStatus) and the boot always falls through to mode::normal::run().
  • Hardens preserve-list handling: rejects empty and ..-containing entries before
    they're joined onto rootfs, closing a path-traversal gap for third-party
    factory-reset.d/*.json files.
  • Fixes a partition-mount leak on a partial factory_reset_mount() failure and an
    is_path_mounted()-error-swallowing bug in the unmount cleanup path.
  • Resolves the vfat boot-partition mount's group ownership dynamically from
    <rootfs>/etc/group instead of a hardcoded gid=6.
  • Updates README.md: factory reset moves from "planned" to "implemented", and the
    startup-flow diagram now shows the BootMode::detect() branch and reset sequence.

Reason

Ports the last selective-preserve factory-reset behavior (mode 1) from the legacy bash
initramfs scripts into the Rust binary, per
docs/superpowers/specs/2026-06-29-factory-reset-mode1-design.md. ODS triggers a reset
by writing the factory-reset bootloader env key; the device must always continue
booting regardless of reset outcome.

Verification

  • cargo test across the standard feature matrix plus factory-reset
    (grub|uboot x gpt|dos x test-utils, with/without factory-reset,
    resize-data, release-image) - all passing.
  • cargo fmt -- --check and cargo clippy --tests -- -D warnings clean.
  • cargo audit - no new advisories.
  • Added unit tests for preserve-path validation (empty / .. traversal / normal
    paths) and the dynamic vfat GID lookup (missing file, missing entry, configured
    value).
  • Manual hardware verification of the full reset sequence per the design spec's
    test table is still outstanding.

Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
- Add Path to imports (alongside PathBuf)
- Collapse nested if-let chains with && per clippy::collapsible_if
- Replace magic number 1 with SUPPORTED_RESET_MODE constant

Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Add BootMode::FactoryReset variant and implement detect() logic to:
- Read factory-reset key from bootloader environment
- Parse JSON config (mode, preserve list)
- Gracefully fall back to Normal on invalid JSON or env read errors
- Add comprehensive test coverage for all detect() paths

Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
- Replace unreachable RestoreFailed match arm with explanatory comment;
  restore_all() accumulates path failures as PartialFailure, never propagates
  RestoreFailed via ?
- Map MountError/ReformatFailed/Io to Error status code (not ConfigError);
  ConfigError is now reserved for MissingField only
- Add spec comment explaining the infinite-reset-loop trade-off on persistent
  set_env failure
- Add comment explaining why factory partition is re-mounted during restore phase

Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Mount /boot with gid=6,fmask=0002,dmask=0002,allow_utime=0020 so the
disk group can write the bootloader environment file (grubenv/uboot-env).
Previously mounted with default fmask/dmask=0022, which prevented
group-write access.

Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
…lidation

Fully implements Factory Reset Mode 1, including selective-preserve backup, reformatting of `data` and `etc` partitions, and restore.

This change also:
- Updates the VFAT boot partition mount to dynamically resolve the `disk` group GID from `/etc/group` for improved compatibility and robustness.
- Introduces validation for factory reset preserve list paths, preventing directory traversal vulnerabilities.
- Refines the mount and unmount sequence during the reset process for better error handling and adherence to the factory reset specification (e.g., omitting the factory partition during the restore phase).
- Updates `README.md` with the new feature description, a detailed flowchart, and feature status.
@JoergZeidler JoergZeidler requested a review from JanZachmann July 3, 2026 07:18
Keeps the startup-flow diagram's color coding reserved for terminal
outcomes (success/reboot/halt/shell), matching every other process
step which uses the default node style.

Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Resolves ambiguity where the `FRESET` identifier was used for both the entry point to the factory reset process and for the subgraph representing it. The entry point is now `FCLEAR`.

Also, clarifies the flow out of the factory reset process, connecting `FSTATUS` (the final status update) to `MREM` for improved accuracy.

@JanZachmann JanZachmann 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.

Automated multi-aspect review (factory-reset mode 1)

Review across correctness, silent-failure, error handling, comments, tests, type design, and spec conformance. Inline comments follow, tagged [Critical] / [Important] / [Suggestion].

Spec conformance: the implementation is faithful to 2026-06-29-factory-reset-mode1-design.md. Note two of the critical findings below (brick-on-partial-reformat, lost factory /etc defaults) are also latent in the spec itself — fixing them needs a spec update, not just code.

Type design (not inline — FactoryResetStatus lines are outside the diff): FactoryResetStatus (src/runtime/omnect_device_service.rs) has pub fields with no enforced relationship between status and error/context/paths, so illegal combinations (Success + error: Some, Error + error: None) are representable. Consider an enum payload per outcome with a flat Serialize impl, or at least document that the producer is the sole guarantor of consistency. FactoryResetStatusCode, RestoreResult, and IncludeFactory are exemplary — no change.

Scope: commit 6f2b1c0 (vfat boot-mount group-write options) is unrelated to factory reset and not in the plan — flag for reviewer awareness.

Strengths: faithful spec conformance, path-traversal hardening beyond spec (with tests), ContinueDegraded classification, crate:: imports, named constants, no bare unwrap() in production paths, strong leaf-level test coverage, good "why"-focused comments.

Comment thread src/mode/factory_reset/mod.rs Outdated
})?;

reformat_ext4(data_dev, "data")?;
reformat_ext4(etc_dev, "etc")?;

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.

[Critical] A partial reformat can brick the device — violates "a failed reset must never block boot".

If reformat_ext4(data) succeeds but reformat_ext4(etc) fails partway, run_reset returns Err → classified ContinueDegradedrun() calls normal::run. Normal boot then runs a strict mount() on the half-mkfs'd etc partition → FilesystemError::MountFailednormal.rs:57 propagates via ?recovery_class maps it to Fatal (error.rs:65) → infinite loop on a release image. The data partition is already wiped and the tmpfs backup is discarded at switch_root: total preserved-data loss plus a brick — the exact scenario the design forbids.

The factory-reset feature owns the destructive window. After the first reformat starts, a mount/overlay failure must be absorbed into ContinueDegraded: verify the reformatted partitions mount before handing off to normal::run, or re-mkfs to a known-good empty state and continue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed the chain: MountFailed on a corrupted etc partition classifies Fatal (error.rs:65), and mount_result? in normal.rs:57 propagates before create_ods_runtime_files runs — so on a release image this halts before any status is ever written to disk.

Fixed in cc63354 + c952d77 for the dominant realistic trigger: run_reset now always includes factory on the restore-phase remount (see reply below), and any failure from that point on resolves to a FactoryResetStatus with data_wiped: true instead of propagating — and mount_remaining_partitions now guards against re-mounting an already-mounted partition (c952d77), closing the EBUSY path a failed cleanup could hit.

Residual case not fixed: if mkfs.ext4 itself genuinely corrupts the partition (as opposed to leaving a stale mount), normal::run's mount_remaining_partitions still hits a real MountFailedFatal → halt, and the status still won't reach disk before that halt. Verifying-then-degrading here would mean adding a new fallback boot mode (e.g. mount etc read-only from factory defaults, skip the overlay) — that's a real feature, not a patch, and needs a design decision rather than something to invent unilaterally while triaging review comments. Flagged in the local review-analysis doc for a decision.

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.

Agreed the realistic triggers are handled (mount guard c952d77 + data_wiped status cc63354). Still pushing on the residual: a genuine mkfs.ext4 corruption of etc leaves normal::run mounting an unmountable partition → MountFailedFatal → halt, and that halt is before create_ods_runtime_files, so the new data_wiped status never reaches disk in exactly that case — still a brick plus a silent outcome. Worth weighing re-mkfs etc to a known-good empty state on a failed remount (keeps it bootable, factory reseeds /etc) as a lighter path than a whole new boot mode. Keeping this thread open until the design decision is made — this belongs in a tracked issue, not only the local review doc.

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.

Now tracked in #17 (flagged as the higher-priority safety item there).

Comment thread src/mode/factory_reset/mod.rs Outdated
Comment thread src/mode/factory_reset/mod.rs
Comment thread src/mode/factory_reset/config.rs Outdated
Comment thread src/mode/factory_reset/config.rs Outdated
Comment thread src/mode/factory_reset/mod.rs Outdated
Comment thread src/mode/factory_reset/mod.rs
return Ok(());
}

let entries = std::fs::read_dir(&dir).map_err(|e| {

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.

[Suggestion] I/O errors misclassified as InvalidConfig. A transient failure reading the config dir/files maps to InvalidConfigrun() reports status code Invalid, telling ODS the config is malformed when the real cause is I/O. Safe for data (aborts before reformat), but misleads diagnosis. Consider a separate Io mapping. (Same at the read_to_string on line 129 and the factory-reset.json reads.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed, and confirmed pre-existing (predates this session — same pattern throughout build_preserve_list/collect_application_paths before this file was touched). Not fixed in this pass — it's a diagnostics-quality gap, not a correctness/data-loss issue, since it still safely aborts before any reformat either way. Lower priority than the silent-skip fixes; left open in the local review-analysis doc.

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.

Agreed — diagnostics-quality, safe (aborts before reformat). Tracked in #17. Leaving open.

Comment thread src/mode/factory_reset/config.rs Outdated
Comment thread src/mode/factory_reset/mod.rs Outdated
…atus

The restore-phase remount excluded factory, but setup_etc_overlay needs it
mounted to reseed etc's freshly-wiped overlay upper dir with factory
defaults before restore_all overlays preserved paths on top. Skipping it
left the upper dir permanently unseeded once any preserved path populated
it. The now-pointless IncludeFactory split is removed; both mount passes
always include factory. Corrected the design spec's step 8, which had the
same oversight.

Also splits run_reset at the point the first reformat begins: failures
before that are safe pre-reformat aborts (unchanged), failures at or after
are resolved into FactoryResetStatus with a new data_wiped flag and the
preserve list populated, so ODS/cloud can distinguish a safe no-op abort
from a failure after data was already wiped.

Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
factory/cert/etc/data mounts had no idempotency check, unlike the boot
partition in mount_core_partitions. A mount leaked by any earlier failed
step (e.g. an aborted factory reset whose cleanup unmount also failed)
would hit mount(2) again on an already-mounted target, fail as
MountFailed, and get classified Fatal — turning a leaked mount into a
halt on a release image. Each mount is now skipped with a warning if
already mounted, matching the existing boot-partition pattern.

Also corrects a stale comment on the vfat boot mount: the group is
resolved dynamically via lookup_disk_gid with a gid=6 fallback, not
hardcoded.

Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Non-string array elements and unreadable factory-reset.d directory
entries were silently skipped (as::str() returning None, or
entries.flatten() dropping Err results). Given a preserve list is what
survives a reformat, silently dropping an entry means the intended path
is wiped and restore_all still reports Success. Both now hard-fail with
InvalidConfig, which is safe since they run before any reformat.

Also swaps unwrap_or_else(|| unreachable!()) for the equivalent, clearer
expect().

Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
@JoergZeidler JoergZeidler requested a review from mlilien July 3, 2026 13:05
Comment thread src/filesystem/boot_sequence.rs Outdated
Comment thread src/mode/factory_reset/config.rs Outdated
Comment thread src/mode/factory_reset/mod.rs Outdated
Comment thread src/mode/factory_reset/mod.rs Outdated
Shorten three doc comments flagged as too verbose in PR review:
validate_preserve_path, run_reset, and the restore-phase remount call.
Keep the "why", drop restated context already evident from the code.

Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Shorten the mount-idempotency explanation flagged as too verbose in PR
review. Keep the "why" (guards against a leaked mount escalating to a
fatal MountFailed), drop the restated convention citation and error
classification detail already evident from the code.

Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
@JoergZeidler JoergZeidler requested a review from mlilien July 6, 2026 09:07
Comment thread src/filesystem/boot_sequence.rs Outdated
Comment thread src/filesystem/mount.rs Outdated
Comment thread src/filesystem/mount.rs Outdated
Comment thread src/filesystem/mount.rs Outdated
Comment thread src/filesystem/mount.rs Outdated
Comment thread src/filesystem/mount.rs Outdated
Comment thread src/filesystem/overlayfs.rs
Comment thread src/mode/factory_reset/mod.rs
Comment thread src/mode/factory_reset/mod.rs Outdated
Comment thread src/mode/factory_reset/mod.rs Outdated
…d gid handling

Addresses @mlilien's review findings on PR omnect#16:
- Remove a boot_sequence.rs comment restating detail already documented on
  MountOptions::vfat()/VFAT_BOOT_DATA in mount.rs.
- Revert the disk-group GID from a runtime /etc/group lookup back to a
  hardcoded constant: this crate only ever boots omnect's own Yocto-built
  images, where the disk group's GID is fixed by the group recipe, so the
  three-branch fallback lookup (missing file, missing entry, parse failure)
  added no value and only added boot-time attack surface and doc-comment
  confusion about its scope.
- Fix a misleading doc comment on CP_CMD: it described the -a flag as a
  property of the constant, but the flag is applied per call site and isn't
  even uniform across all three call sites.

Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
Move factory-reset's bespoke mount/fsck/unmount logic into the shared
filesystem module: mount_tracked_partition (with fsck, fixing a gap where
factory-reset previously skipped fsck before backing up data/etc),
unmount_tracked, and setup_etc_overlay_tracked/setup_data_overlay_tracked.
factory_reset_mount now only supplies factory-reset-specific partition
choices instead of duplicating mount mechanics.

Also fixes a partial-mount-leak: the tracked overlay setup functions now
record each sub-mount target immediately after it succeeds, instead of only
after the whole overlay setup call returns Ok, so a partially-failed overlay
setup no longer leaves an untracked mount behind on cleanup.

Dropped the is_path_mounted guard in the unmount loop: mounts is only ever
populated immediately after a successful mount in a single-threaded flow, so
the guard never observed an unmounted tracked path.

Signed-off-by: Joerg Zeidler <62105035+JoergZeidler@users.noreply.github.com>
@JoergZeidler JoergZeidler requested a review from mlilien July 7, 2026 09:47
The previous comment included implementation details about cross-module usage and sharing of `CP_CMD`. This change removes that specific context, making the comment more concise and focused on the constant's immediate purpose, aligning with prior efforts to trim verbose documentation.
@JoergZeidler JoergZeidler merged commit 125670a into omnect:main Jul 8, 2026
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