Skip to content

fix(p2p): write the generated man-p2p runtime config with mode 0600 - #37

Merged
newfish merged 1 commit into
metaid-developers:mainfrom
WuFenG-Hub:fix/man-p2p-runtime-config-mode-0600
Sep 15, 2026
Merged

newfish merged 1 commit into
metaid-developers:mainfrom
WuFenG-Hub:fix/man-p2p-runtime-config-mode-0600

Conversation

@WuFenG-Hub

Copy link
Copy Markdown
Contributor

Symptom

When the man-p2p config needs a local override, IDBots generates
man-p2p-runtime-config.toml inside the man-p2p data dir. That file is the fully
resolved
man-p2p config — it embeds the base config verbatim, including
plaintext third-party RPC credentials
— yet it was written through
fs.writeFileSync without an explicit mode, so it landed at 0o666 & ~umask
= 0644 (-rw-r--r--) under the common umask 0022.

The only credential-bearing file in that directory was therefore the loosest one;
its sibling identity.key is created 0600 (-rw-------) by the same code base.

Reproducible before/after on an isolated data dir (umask 0022):

$ node probe-runtime-config-mode.mjs <dist-electron>/main/services/p2pIndexerService.js <tmp>/unpatched
mode(octal)      = 0644
umask(octal)     = 0022
-rw-r--r-- wufeng:staff 149 bytes .../unpatched/man-p2p/man-p2p-runtime-config.toml   # 08-probe-unpatched.log

$ node probe-runtime-config-mode.mjs <dist-electron>/main/services/p2pIndexerService.js <tmp>/patched
mode(octal)      = 0600
umask(octal)     = 0022
-rw------- wufeng:staff 147 bytes .../patched/man-p2p/man-p2p-runtime-config.toml      # 06-probe-patched.log

The original leak was also observed on a real install ($HOME/Library/Application Support/IDBots/man-p2p/man-p2p-runtime-config.toml, 1362 bytes) — v1 of this body
quoted that stat as -rw-r--r--. That particular capture is no longer
re-verifiable
: while the fix was still uncommitted, that live file's mode was
changed to 0600 out-of-band by the machine operator as an immediate local
stop-gap while this fix was still uncommitted (inode ctime 12:22:17, mtime
unchanged at 10:44:36 — a chmod, not a rewrite; the file's content was untouched). Treat the live
stat as narrative context only; the load-bearing evidence is the isolated
red/green below, which is reproducible at any time.

Root cause

src/main/services/p2pIndexerService.ts:470 at 62577cb1 (v0.9.0):

fs.mkdirSync(path.dirname(runtimeConfigPath), { recursive: true });
fs.writeFileSync(runtimeConfigPath, runtimeConfig, 'utf8'); // :470 — no mode
return runtimeConfigPath;

fs.writeFileSync without an explicit mode uses 0o666 & ~umask; under umask
0022 that is 0644.

Fix

Pass an explicit mode, and tighten the file afterwards:

/**
 * The generated runtime config embeds the whole resolved man-p2p config,
 * including plaintext third-party RPC credentials, so it must stay owner-only —
 * the same convention the sibling `identity.key` follows. Without an explicit
 * mode, `fs.writeFileSync` falls back to `0o666 & ~umask` (0644 under the usual
 * umask), which is wider than the credential-free files beside it.
 */
const RUNTIME_CONFIG_FILE_MODE = 0o600;                     // :453

// ...
fs.writeFileSync(runtimeConfigPath, runtimeConfig, {        // :479
  encoding: 'utf8',
  mode: RUNTIME_CONFIG_FILE_MODE,
});
// `mode` only takes effect when the file is created, so tighten runtime configs
// that earlier versions already wrote world-readable.
fs.chmodSync(runtimeConfigPath, RUNTIME_CONFIG_FILE_MODE);   // :485

Why the extra chmodSync. fs.writeFileSync applies mode only when it
creates the file
. On any existing installation the runtime config already
exists with 0644, so passing mode alone would fix new installs and silently
leave every upgraded install still leaking credentials — the worst kind of
half-fix, because the symptom disappears from fresh test runs. The chmodSync
makes the post-condition unconditional: after resolveRuntimeConfigPath()
returns a runtime config path, that file is 0600.
It touches exactly that one
file and nothing else (no directory sweep, no recursive chmod).

Change size: 1 file changed, 16 insertions(+), 1 deletion(-) in
src/main/services/p2pIndexerService.ts, plus one new test file and its
.gitignore whitelist line (this repo whitelists test files individually via
!tests/<name>; tests/* is otherwise ignored).

Scope

  • Only the generated runtime config is affected.
  • The user-provided base config (resolveMainConfigPath()) is never written
    and never chmoded: when no override is needed, the early return at :466
    (baseline) still hands back mainConfigPath and no runtime file is created.
    tests/p2pRuntimeConfigMode.test.mjs pins both the content and the mode
    of the base config so this stays true.
  • No other file's permissions are touched, and no read path is changed.

Verification

New regression test: tests/p2pRuntimeConfigMode.test.mjs. It loads the
compiled main-process module (dist-electron/main/services/p2pIndexerService.js),
so red/green was proven with a recompile in between, and the patch marker
RUNTIME_CONFIG_FILE_MODE was used to prove the compiled artifact actually
changed (not just the TypeScript source).

Tests (4):

  1. control — under umask 0022 a plain writeFileSync is observable as
    0644, so the mode assertion is not vacuous;
  2. a freshly generated runtime config is 0600;
  3. a pre-existing world-readable (0644) runtime config is tightened to 0600;
  4. the no-override path leaves the base config's content and mode untouched.

Red — source reverted to 62577cb1, recompiled

$ git checkout 62577cb1 -- src/main/services/p2pIndexerService.ts
$ npx --no-install tsc --project electron-tsconfig.json && node scripts/copy-electron-js.cjs
[compile:electron] copied JavaScript runtime modules to dist-electron/main
COMPILE_EXIT=0

$ grep -c 'RUNTIME_CONFIG_FILE_MODE' dist-electron/main/services/p2pIndexerService.js
0                       # exit code 1 (no match) — the compiled artifact really changed

$ node --test tests/p2pRuntimeConfigMode.test.mjs
✔ control: the mode assertion can observe a 0644 file (assertion is not vacuous)
✖ resolveRuntimeConfigPath writes a freshly generated man-p2p runtime config with mode 0600
✖ resolveRuntimeConfigPath tightens a pre-existing world-readable runtime config to 0600
✔ resolveRuntimeConfigPath leaves the user-provided base config untouched ...
ℹ tests 4   ℹ pass 2   ℹ fail 2      # exit code 1
# assertion detail: actual 420 (0o644) !== expected 384 (0o600)

Green — patch applied, recompiled

$ git checkout HEAD -- src/main/services/p2pIndexerService.ts
$ npx --no-install tsc --project electron-tsconfig.json && node scripts/copy-electron-js.cjs
COMPILE_EXIT=0

$ grep -c 'RUNTIME_CONFIG_FILE_MODE' dist-electron/main/services/p2pIndexerService.js
3

$ node --test tests/p2pRuntimeConfigMode.test.mjs
✔ control: the mode assertion can observe a 0644 file (assertion is not vacuous)
✔ resolveRuntimeConfigPath writes a freshly generated man-p2p runtime config with mode 0600
✔ resolveRuntimeConfigPath tightens a pre-existing world-readable runtime config to 0600
✔ resolveRuntimeConfigPath leaves the user-provided base config untouched ...
ℹ tests 4   ℹ pass 4   ℹ fail 0      # exit code 0

Related existing suites, lint

$ node --test tests/p2pRuntimeConfigSync.test.mjs        # tests 2  / pass 2  / fail 0 -> exit 0
$ node --test tests/p2pIndexerServiceRecovery.test.mjs   # tests 6  / pass 6  / fail 0 -> exit 0
$ node --test tests/p2pConfigService.test.mjs            # tests 12 / pass 12 / fail 0 -> exit 0
$ npx --no-install eslint src/main/services/p2pIndexerService.ts -> exit 0

Notes for reviewers

  • The mode is deliberately a hard-coded 0600, not derived from the ambient
    umask: the file carries credentials, so it must be owner-only regardless of the
    host umask.
  • The fix does not attempt to change the mode of anything else in the data dir
    (man_base_data_pebble/ stays 0755, identity.key is untouched).

The generated man-p2p-runtime-config.toml embeds the fully resolved man-p2p
config, including plaintext third-party RPC credentials, but fs.writeFileSync
was called without a mode, so the file landed as 0o666 & ~umask (0644 under the
usual umask 0022) - wider than the sibling identity.key (0600).

Pass an explicit 0o600 mode, and chmod afterwards because writeFileSync's mode
only applies when the file is created (existing installs already have a leaked
0644 runtime config). The user-provided base config is untouched: the
no-override early return is unchanged.

Adds tests/p2pRuntimeConfigMode.test.mjs (fresh write, pre-existing 0644 file,
untouched base config, non-vacuous mode assertion) and whitelists it in
.gitignore.
@newfish
newfish merged commit 55ed8c6 into metaid-developers:main Sep 15, 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.

2 participants