Skip to content

fix: audit 2026-10-10 - open rc port visible and closable, no invisible-backup swaps, FD leak, log trim - #22

Merged
mbeczynski merged 5 commits into
mainfrom
fix/audyt2-cloudmachine
Oct 11, 2026
Merged

mbeczynski merged 5 commits into
mainfrom
fix/audyt2-cloudmachine

Conversation

@mbeczynski

Copy link
Copy Markdown
Contributor

Second-round audit of CloudMachine (2026-10-10), on top of v1.3.8. One commit per finding; every fix has a regression test.

What changes

N-K1 (critical): the open remote-control port is visible and can be closed without a reboot

A mount started by 1.3.7 or older survives brew upgrade and keeps taking unauthenticated commands on 127.0.0.1:5572 (including operations/purge, with the trash off) until the Mac restarts. Before this PR only one drive-status line said so.

  • One check, DriveBufferService.legacyPortOpen: no socket, plus a mount, plus something accepting connections on the port. The last condition separates the old mount from a socket deleted under a new one. The window, the watchdog and the CLI all use it.

  • Window: a red card in every section with the explanation, the prepare-shutdown command and a Restart the mount button. The button asks for confirmation and is disabled during a backup. In this state the green badge goes away, and the headline and the menu bar say why.

  • Watchdog: has its own notification, OpenPortAlert. This is not a backup failure, so it does not change the exit code. It fires once, then after 1, 2 and 4 days, then every 7 days. A notification that did not show is retried on the next run.

  • restart-mount / the button (MountRestart):

    1. Refuses if Time Machine is running or if that is unknown.
    2. Detaches and drains exactly like prepare-shutdown, and looks at erroredFiles in the result.
    3. Sends launchctl kill SIGTERM to the mount job, so KeepAlive starts the new code. If launchd cannot spawn the job, it is reloaded once it has exited.
    4. Waits for the private socket, then attaches the image.

    The image lock is held the whole time. If anything is doubtful before the stop, the mount is left alone and the image is attached again. bootout and kickstart -k were rejected; the reasons are in the file header.

N-W1 (high): a visible backup is never swapped for an invisible one

  • Saving OAuth credentials no longer suggests configure-remote --replace-existing. On a Mac that is already connected, the panel now says to keep the existing connection.
  • Replacing an existing remote now needs --replace-existing --accept-scope-change. Even then, the new token lists the image on an in-memory :drive: remote (token passed through the environment) before rclone.conf is written. If the old connection saw the image and the new one does not, nothing is written.
  • create-image refuses when Drive shows no image but this Mac remembers one. The traces are the known-image marker (written on create and attach) and the image in the vfs cache. The message says "check the token's permissions" and does not create a second, empty backup.

W1 remainder (S4): a missing rclone.conf is not "no remote"

  • If listremotes lists nothing, rclone config file says the file does not exist, and this Mac has cached Drive folders (cache/vfs/gdrive{…}/CloudMachine/<folder>), the answer is now nil (unknown) instead of false.
  • A cached mac-studio folder alone keeps a legacy Mac in mac-studio.

Medium

  • S1 ProcessRunner FDs: the timeout timers held the Process and both pipe readers until timeout + 10 s. They now hold a small box that is emptied when the process exits. Test: 50 calls with timeout: 30 leave at most 10 descriptors open. The old code leaves 100.
  • S3 usage measurement: with a limit set, a measurement older than usageStaleAfter, or no measurement at all, is now the watchdog problem drive-usage-unmeasured. Before, the alarm compared an old reading or stayed silent.
  • N8 rclone.log: the buffer guard trims the log of the running mount above 100 MB. The last 32 MB go to .1 and the live file is truncated. rclone opens the file O_APPEND (checked with lsof +fg: W,AP). The stall and quota checks read the end of .1 together with the live file, so a trim in the middle of a jam does not read as "no jam". Rejected alternatives:
    • writing the tail back into the live file: it races with rclone's appends and breaks the chronological order the detection depends on;
    • rclone's --log-file-max-size: it only applies after the mount restarts, and its backup files get timestamped names.

Low

  • N2: when the mount table cannot be read, the status says no socket - UNKNOWN whether the mount is running.
  • N5: the doc comment is back on MachineIdentity.storedKey.
  • S2: the README and the cask caveats use renacode/tap/cloudmachine.
  • Not done:
    • N1: remoteConfigured starts as false, so the first frame can show "not connected". The CLI refuses anyway.
    • N3: the token is passed in the argv of config create. rclone config create has no other input.
    • N4: the transport is still chosen with fileExists. The new port check removes the false "OPEN" warning for a socket deleted under a new mount.
    • N6/N7: these need an operator decision.

Tests

  • swift build: no warnings in Sources.
  • swift test --skip BackupImageServiceTests: 390 tests, 0 failures (359 before).
  • swift format lint --strict --recursive Sources Tests: clean.
  • Checked by swapping in the old source:
    • testFinishedProcessesReleaseTheirDescriptors fails on old ProcessRunner with 100 descriptors still open.
    • testJamBeforeTheTrimIsStillSeen fails on old DriveBufferService (false, expected true).
  • The other new tests target new API. Their behaviour change is the opposite of an assertion in the old tests; for example, problems(usage: nil, limitGB: 1000) used to be asserted empty.
  • Not run on a live system: the live path of MountRestart (launchctl, socket wait) and the :drive: listing with a fresh token. A production backup runs on the development Mac, so neither was exercised there.

What happens on brew upgrade from 1.3.4 (co się stanie przy brew upgrade z 1.3.4)

  1. The cask replaces the app, and Homebrew reopens it. AgentRepair reloads backup-health, gdrive-attach and buffer-guard. gdrive-buffer (rclone from 1.3.4) keeps running on TCP 5572: no mount restart and no gap in backups.

  2. Within one refresh (10 s), the window shows the red "Remote control of the Drive mount is open" card. The menu bar switches to Attention. The next backup-health run prints a WARNING line and shows one notification.

  3. On its first tick, buffer-guard trims ~/.cloudmachine/rclone.log (about 273 MB): the last 32 MB replace rclone.log.1 and the live file is truncated. rclone carries on writing at the new end.

  4. Within 15 minutes, gdrive-attach reports "Already attached" and writes the known-image marker.

  5. Since fix: audit 2026-10-09 - rclone rc on a private socket, fail-closed remote check, lost process output #21 the 3500 GB limit applies. If rclone size fails for more than 12 h, the watchdog now reports drive-usage-unmeasured; before, it stayed silent.

  6. When the user presses Restart the mount (not during a backup):

    • the image is detached and the queue drained (≥35 s plus the upload);
    • rclone gets SIGTERM and launchd starts the new mount-drive on ~/.cloudmachine/run/rc.sock;
    • the image is attached again.

    For a few minutes Time Machine has no destination, and a watchdog run that lands in that window can report it. If anything has not reached Drive, nothing is stopped. The alternative is still prepare-shutdown followed by a restart of the Mac.

  7. Nothing runs configure-remote or create-image on its own. The new guards only take effect when someone runs them.

Merging this publishes a release (CD). Please do not merge before the review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ww8BFyxe5Efb9pvhshhtig

mbeczynski and others added 5 commits October 11, 2026 09:19
The timeout timers held the Process and both pipe readers until
timeout + 10 s, so every call with a timeout kept two descriptors open
long after the child had exited (+100 for 50 calls; 76-92 pipes in the
running app against a soft limit of 256). The timers now hold a small box
that is emptied in terminationHandler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ww8BFyxe5Efb9pvhshhtig
With a limit set, a failed rclone size left the watchdog comparing the
last stored reading with the limit regardless of its age, or raising
nothing when there was no reading. The limit then guarded nothing and
only the panel turned amber. A reading older than usageStaleAfter (or
none at all) is now a watchdog problem, drive-usage-unmeasured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ww8BFyxe5Efb9pvhshhtig
… stall check

The log was only rotated when the mount started, and the mount runs for
days (257 MB on 10.10, +60 MB a day). The buffer guard now trims it above
100 MB: the last 32 MB go to rclone.log.1 and the live file is truncated
(rclone opens it O_APPEND - checked with lsof - so it keeps writing at the
new end). The stall and quota checks read the tail of .1 together with
the live file when the live file is short, so a trim in the middle of a
jam does not read as 'no jam'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ww8BFyxe5Efb9pvhshhtig
- The panel no longer suggests configure-remote --replace-existing after
  credentials are saved; on a connected Mac it says to keep the
  connection (a drive.file sign-in under another client_id cannot see
  the existing image).
- Replacing an existing remote needs --accept-scope-change as well, and
  even then the new token lists the image (on an in-memory :drive:
  remote, token via the environment) before rclone.conf is written; if
  the old connection saw the image and the new one does not, nothing is
  written.
- create-image refuses when Drive shows no image but this Mac remembers
  one (known-image marker written on create/attach, or the image in the
  vfs cache): 'check the token', not a second empty backup.
- A missing rclone.conf next to cached Drive folders is 'unknown', not
  'no remote'; a cached mac-studio folder alone keeps a legacy Mac in
  mac-studio.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ww8BFyxe5Efb9pvhshhtig
…ut a reboot

A mount started before the private socket survives brew upgrade and keeps
taking unauthenticated commands on 127.0.0.1:5572 until the Mac restarts.
Only one drive-status line said so.

- One check (no socket + mount + something accepts on the port) feeds the
  window, the watchdog and the CLI.
- Window: a red card in every section with the explanation, the
  prepare-shutdown command and a Restart the mount button (confirmation
  dialog, disabled during a backup); the green badge goes, the headline
  and the menu bar say it.
- Watchdog: a notification of its own (not a backup failure, so not in
  the exit code): once, then after 1, 2, 4 and every 7 days; undelivered
  ones are retried.
- restart-mount / the button: refuses while Time Machine runs or when
  that is unknown; detach and drain as prepare-shutdown, then SIGTERM to
  the mount job (KeepAlive starts the new code; a job launchd cannot
  spawn is reloaded once it has exited), wait for the socket, attach.
  Holds the image lock; any doubt before the stop leaves the mount alone
  and the image re-attached.
- drive-status no longer says 'the mount is not running' when the mount
  table could not be read (N2); MachineIdentity doc comment back on its
  symbol (N5); docs use the full cask name (S2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ww8BFyxe5Efb9pvhshhtig
@mbeczynski
mbeczynski merged commit 395628c into main Oct 11, 2026
4 checks passed
@mbeczynski
mbeczynski deleted the fix/audyt2-cloudmachine branch October 11, 2026 08:50
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