fix(app): a saved secret is 0600 on Apple's Arm too, and macOS saves at all

The short reading's H1: open(2) takes its mode among variable arguments,
which Apple's Arm passes on the stack, so bound with three fixed ones it
read the mode from whatever the stack held: a phrase or recovery codes
saved on an Apple silicon Mac could be readable by everyone. It is bound
as variadic now (VarArgs), and every file written is made 0600 through its
descriptor (fchmod), checked.

M1: macOS's sandbox lets an app reach the file the person chose and
nothing beside it, so the file made beside it was refused and nothing
could be saved; Learn's sandbox allowed reading chosen files alone, so its
exports never saved on macOS either. There, writePrivately now writes the
chosen file itself (decided by the developer on 21 Oct): opened without
following a link or waiting on a pipe and without truncating, refused with
nothing written unless fstat finds a file of the own user's, then made
0600 and emptied. Both apps' entitlements grant reading and writing the
files the person chooses. Linux keeps the new file renamed over it.

The reading's nits: the buffer is wiped whatever happens, an interrupted
call is made again (EINTR), the file made beside a chosen one is named
apart from it so a name as long as a folder takes still saves, the folder
is flushed after the rename, and Linux reads a file's type and owner with
statx, alike on every architecture, where __lxstat's version differed
(riscv64's was wrong). The test of another user's file is skipped as root.
The in-place path is tested on Linux; none of it has run on a Mac.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-21 17:57:44 +02:00
commit 290f584329
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -104,12 +104,20 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
compiler and make: sodium's build hook compiles libsodium.
- **Files** (`FileSaver`, `PlatformFileSaver`): keeping a file the app made
where the person chooses (a recovery phrase, recovery codes, an app's
exports): on Linux and macOS the own user's alone from the start
(`writePrivately`: a new file beside it, made 0600 with `O_EXCL`, written
through its descriptor, renamed over the one chosen, never written into;
a symbolic link or another user's file refused), on Windows written in
place with its folder's rights, on iOS without the copy file_picker leaves
in the temporary folder (`forgetPickerCopy`).
exports): on Linux and macOS the own user's alone (`writePrivately`). On
Linux a new file beside it, made 0600 with `O_EXCL`, written through its
descriptor, renamed over the one chosen, never written into, and the
folder flushed. On macOS, whose sandbox lets an app reach the file chosen
and nothing beside it, the file itself, opened without following a link
or waiting on a pipe, refused unless it is a file of the own user's, made
0600 and emptied before anything is written; whoever held it open reads
what is saved. Each app's macOS entitlements grant reading and writing
the files the person chooses (`com.apple.security.files.user-selected.read-write`),
without which nothing could be saved there, Learn's exports neither. On
both, a symbolic link or another user's file is refused with nothing
written. On Windows written in place with its folder's rights, on iOS
without the copy file_picker leaves in the temporary folder
(`forgetPickerCopy`). None of it has run on a Mac yet.
- **Sync** (`lib/src/sync/`): `SyncService` (once at start, shortly after
local changes, on resume and every few minutes, one sync at a time, counting
the changes not on the server yet, `PendingChangesStore`), `AppCompatibility`