hikoutei 0.11.102-dev → 0.11.103-dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/adoptFlow.d.ts.map +1 -1
- package/dist/cli/adoptFlow.js +31 -0
- package/dist/cli/adoptFlow.js.map +1 -1
- package/dist/cli/checkpoint.d.ts +36 -1
- package/dist/cli/checkpoint.d.ts.map +1 -1
- package/dist/cli/checkpoint.js +81 -21
- package/dist/cli/checkpoint.js.map +1 -1
- package/dist/cli/envFileWriter.d.ts +42 -8
- package/dist/cli/envFileWriter.d.ts.map +1 -1
- package/dist/cli/envFileWriter.js +60 -30
- package/dist/cli/envFileWriter.js.map +1 -1
- package/dist/cli/errors.d.ts +6 -5
- package/dist/cli/errors.d.ts.map +1 -1
- package/dist/cli/errors.js +6 -5
- package/dist/cli/errors.js.map +1 -1
- package/dist/cli/gcloudRunner.d.ts +22 -5
- package/dist/cli/gcloudRunner.d.ts.map +1 -1
- package/dist/cli/gcloudRunner.js +168 -0
- package/dist/cli/gcloudRunner.js.map +1 -1
- package/dist/cli/keyProvision.d.ts +22 -44
- package/dist/cli/keyProvision.d.ts.map +1 -1
- package/dist/cli/keyProvision.js +212 -103
- package/dist/cli/keyProvision.js.map +1 -1
- package/dist/cli/setupFlow.d.ts +2 -7
- package/dist/cli/setupFlow.d.ts.map +1 -1
- package/dist/cli/setupFlow.js +301 -55
- package/dist/cli/setupFlow.js.map +1 -1
- package/dist/cli/setupPathCollision.d.ts +11 -3
- package/dist/cli/setupPathCollision.d.ts.map +1 -1
- package/dist/cli/setupPathCollision.js +9 -6
- package/dist/cli/setupPathCollision.js.map +1 -1
- package/dist/cli/setupPaths.d.ts +2 -0
- package/dist/cli/setupPaths.d.ts.map +1 -1
- package/dist/cli/setupPaths.js +9 -0
- package/dist/cli/setupPaths.js.map +1 -1
- package/dist/sheets/providers/google-sheets-api/model/batchBuilder.d.ts +10 -0
- package/dist/sheets/providers/google-sheets-api/model/batchBuilder.d.ts.map +1 -1
- package/dist/sheets/providers/google-sheets-api/model/batchBuilder.js +25 -36
- package/dist/sheets/providers/google-sheets-api/model/batchBuilder.js.map +1 -1
- package/dist/sheets/providers/google-sheets-api/model/planner.d.ts.map +1 -1
- package/dist/sheets/providers/google-sheets-api/model/planner.js +0 -3
- package/dist/sheets/providers/google-sheets-api/model/planner.js.map +1 -1
- package/dist/sheets/providers/google-sheets-api/operations/fastAppend.d.ts.map +1 -1
- package/dist/sheets/providers/google-sheets-api/operations/fastAppend.js +3 -13
- package/dist/sheets/providers/google-sheets-api/operations/fastAppend.js.map +1 -1
- package/dist/sheets/providers/google-sheets-api/operations/rowChecks.js +9 -10
- package/dist/sheets/providers/google-sheets-api/operations/rowChecks.js.map +1 -1
- package/package.json +1 -1
|
@@ -59,11 +59,20 @@
|
|
|
59
59
|
* unsafe types or permission failures reject the run without touching
|
|
60
60
|
* foreign entries. The key create runs gcloud with a RELATIVE `key.json`
|
|
61
61
|
* destination from the staging directory as the subprocess working
|
|
62
|
-
* directory (runner `cwd
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
62
|
+
* directory (runner `cwd`, expected identity, and a pinned directory fd),
|
|
63
|
+
* with the staging directory re-verified through a no-follow descriptor
|
|
64
|
+
* immediately before spawn and its identity checked again immediately
|
|
65
|
+
* after spawn, before the result is recorded or staged output is trusted.
|
|
66
|
+
* The isolated Node child (`process.execPath`, parent CWD untouched)
|
|
67
|
+
* compares its `statSync('.')` with `fstatSync(3)` for the inherited pinned
|
|
68
|
+
* fd. A replacement cannot pass by reusing the old inode, and gcloud
|
|
69
|
+
* inherits the verified object-bound CWD — the
|
|
70
|
+
* staging-cwd write window (#673) is closed with portable built-ins, no
|
|
71
|
+
* native binding. The post-spawn identity re-check stays as defense in
|
|
72
|
+
* depth. A transient swap-back (the
|
|
73
|
+
* replacement removed before the post-spawn check) leaves no key in the
|
|
74
|
+
* real directory, so the bounded settlement below ends `uncertain`
|
|
75
|
+
* without ever trusting the foreign directory. The same parent validation runs BEFORE the staged key is ever read during reconciliation: `key.json` is never inspected,
|
|
67
76
|
* opened, chmod'ed, or read unless its deterministic parent is absent or a
|
|
68
77
|
* plain directory verified/secured to 0700 through the no-follow
|
|
69
78
|
* descriptor, and a symlinked or non-directory stage parent fails closed
|
|
@@ -306,8 +315,7 @@ export type KeySettleOutcome = {
|
|
|
306
315
|
*/
|
|
307
316
|
export declare function settleServiceAccountKey(runner: GcloudRunner, executed: PlannedCommand[], input: KeySettleInput): Promise<KeySettleOutcome>;
|
|
308
317
|
/**
|
|
309
|
-
* Creates the deterministic staging directory
|
|
310
|
-
* identity.
|
|
318
|
+
* Creates or secures the deterministic staging directory and pins it open.
|
|
311
319
|
*
|
|
312
320
|
* A pre-existing entry is accepted only when it is a real directory (a
|
|
313
321
|
* crash between the checkpoint write and the create leaves an empty owned
|
|
@@ -317,23 +325,23 @@ export declare function settleServiceAccountKey(runner: GcloudRunner, executed:
|
|
|
317
325
|
* platform supports it (the mode is applied and verified on the open
|
|
318
326
|
* descriptor, never via a check-then-chmod path race), and any unsafe
|
|
319
327
|
* type/permission failure rejects the run without touching foreign
|
|
320
|
-
* entries.
|
|
321
|
-
*
|
|
322
|
-
* pre-existing-directory
|
|
323
|
-
* from the one fresh create).
|
|
328
|
+
* entries. Its open descriptor pins the directory object through process
|
|
329
|
+
* launch, preventing inode reuse from making a replacement appear identical.
|
|
330
|
+
* Exported so tests can exercise the pre-existing-directory path directly.
|
|
324
331
|
*/
|
|
325
332
|
export declare function prepareStageDir(keyPath: string, keyMarker: string): PreparedStageDir;
|
|
326
333
|
/**
|
|
327
334
|
* Result of preparing the deterministic staging directory.
|
|
328
335
|
*
|
|
329
|
-
* `ok` carries
|
|
330
|
-
*
|
|
331
|
-
*
|
|
336
|
+
* `ok` carries a pinned descriptor and its device/inode identity so the
|
|
337
|
+
* subprocess wrapper can verify its working directory against the exact
|
|
338
|
+
* open filesystem object.
|
|
332
339
|
*/
|
|
333
340
|
export type PreparedStageDir = {
|
|
334
341
|
readonly status: "ok";
|
|
335
342
|
readonly dev: number;
|
|
336
343
|
readonly ino: number;
|
|
344
|
+
readonly fd: number;
|
|
337
345
|
} | {
|
|
338
346
|
readonly status: "error";
|
|
339
347
|
readonly error: SetupErrorResult;
|
|
@@ -354,36 +362,6 @@ export interface KeyCleanupFs {
|
|
|
354
362
|
fstatSync(fd: number): Stats;
|
|
355
363
|
closeSync(fd: number): void;
|
|
356
364
|
}
|
|
357
|
-
/**
|
|
358
|
-
* Ownership-bound, crash-resumable staged cleanup, only after a verified
|
|
359
|
-
* installation.
|
|
360
|
-
*
|
|
361
|
-
* The final key path is verified with lstat (never following a symlink) and
|
|
362
|
-
* must be a regular file; a final symlink is refused and the staged
|
|
363
|
-
* credential is retained. Before any deletion the deterministic stage
|
|
364
|
-
* directory is atomically quarantined: renamed to the deterministic private
|
|
365
|
-
* cleanup sibling path derived from the SAME persisted key marker (never an
|
|
366
|
-
* unrecoverable random name), with the device/inode captured before the
|
|
367
|
-
* rename re-verified after it — a source replaced between the capture and
|
|
368
|
-
* the rename fails closed and nothing is deleted. Inside the quarantined
|
|
369
|
-
* 0700 directory the staged entry is re-checked as a regular file with the
|
|
370
|
-
* same device/inode as the no-follow final key immediately before its
|
|
371
|
-
* unlink; any mismatch, foreign, or non-empty entry is preserved (the
|
|
372
|
-
* directory is moved back to the stage path so the user finds the state
|
|
373
|
-
* where the docs say it is). The empty quarantined directory is then
|
|
374
|
-
* rmdir'd. Never recursive-delete.
|
|
375
|
-
*
|
|
376
|
-
* Crash-resume boundaries: a crash-left cleanup directory with the matching
|
|
377
|
-
* staged hardlink is finished (verify against the no-follow final, unlink,
|
|
378
|
-
* rmdir); an empty cleanup directory (crash after the staged unlink) is
|
|
379
|
-
* rmdir'd; an absent stage + cleanup pair means the cleanup already
|
|
380
|
-
* finished (success); both existing at once fails closed. The setup lock is
|
|
381
|
-
* held for the whole run — a second cooperating setup process cannot
|
|
382
|
-
* acquire it concurrently, so no other run can be touching these
|
|
383
|
-
* deterministic paths — and Node offers no literal unlinkat without a raw
|
|
384
|
-
* libuv binding, so this quarantine + post-rename identity design is the
|
|
385
|
-
* permitted boundary for the pathname unlink race.
|
|
386
|
-
*/
|
|
387
365
|
export declare function cleanupOwnedStage(keyPath: string, keyMarker: string, fs?: KeyCleanupFs): SetupErrorResult | null;
|
|
388
366
|
export {};
|
|
389
367
|
//# sourceMappingURL=keyProvision.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keyProvision.d.ts","sourceRoot":"","sources":["../../../../../../src/keyProvision.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"keyProvision.d.ts","sourceRoot":"","sources":["../../../../../../src/keyProvision.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/keyProvision.js
CHANGED
|
@@ -59,11 +59,20 @@
|
|
|
59
59
|
* unsafe types or permission failures reject the run without touching
|
|
60
60
|
* foreign entries. The key create runs gcloud with a RELATIVE `key.json`
|
|
61
61
|
* destination from the staging directory as the subprocess working
|
|
62
|
-
* directory (runner `cwd
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
62
|
+
* directory (runner `cwd`, expected identity, and a pinned directory fd),
|
|
63
|
+
* with the staging directory re-verified through a no-follow descriptor
|
|
64
|
+
* immediately before spawn and its identity checked again immediately
|
|
65
|
+
* after spawn, before the result is recorded or staged output is trusted.
|
|
66
|
+
* The isolated Node child (`process.execPath`, parent CWD untouched)
|
|
67
|
+
* compares its `statSync('.')` with `fstatSync(3)` for the inherited pinned
|
|
68
|
+
* fd. A replacement cannot pass by reusing the old inode, and gcloud
|
|
69
|
+
* inherits the verified object-bound CWD — the
|
|
70
|
+
* staging-cwd write window (#673) is closed with portable built-ins, no
|
|
71
|
+
* native binding. The post-spawn identity re-check stays as defense in
|
|
72
|
+
* depth. A transient swap-back (the
|
|
73
|
+
* replacement removed before the post-spawn check) leaves no key in the
|
|
74
|
+
* real directory, so the bounded settlement below ends `uncertain`
|
|
75
|
+
* without ever trusting the foreign directory. The same parent validation runs BEFORE the staged key is ever read during reconciliation: `key.json` is never inspected,
|
|
67
76
|
* opened, chmod'ed, or read unless its deterministic parent is absent or a
|
|
68
77
|
* plain directory verified/secured to 0700 through the no-follow
|
|
69
78
|
* descriptor, and a symlinked or non-directory stage parent fails closed
|
|
@@ -657,15 +666,20 @@ async function settleKeyPass(runner, executed, input, createIssued) {
|
|
|
657
666
|
return { status: "error", error: prepared.error };
|
|
658
667
|
}
|
|
659
668
|
// The gcloud create runs with the staging directory as the subprocess
|
|
660
|
-
// working directory and a RELATIVE `key.json` destination
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
669
|
+
// working directory and a RELATIVE `key.json` destination. Immediately
|
|
670
|
+
// before the spawn, the directory is re-verified (no-follow
|
|
671
|
+
// descriptor, type, identity, owner-only mode) against the identity
|
|
672
|
+
// captured by prepareStageDir; the runner then binds the child CWD to
|
|
673
|
+
// that verified object through an isolated Node wrapper (chdir +
|
|
674
|
+
// statSync('.') identity check, gcloud inherits the bound directory
|
|
675
|
+
// with no `cwd` option), so a replacement at the staging pathname
|
|
676
|
+
// cannot redirect the credential write. Immediately AFTER the spawn
|
|
677
|
+
// returns the identity is re-checked again before the result is
|
|
678
|
+
// recorded, as defense in depth.
|
|
666
679
|
const stageDir = keyStageDir(input.keyPath, input.keyMarker);
|
|
667
680
|
const stillSecure = verifyStageDirBeforeSubprocess(stageDir, prepared);
|
|
668
681
|
if (stillSecure !== null) {
|
|
682
|
+
closePreparedStageDir(prepared.fd);
|
|
669
683
|
return { status: "error", error: stillSecure };
|
|
670
684
|
}
|
|
671
685
|
const keyCreateCommand = [
|
|
@@ -681,15 +695,35 @@ async function settleKeyPass(runner, executed, input, createIssued) {
|
|
|
681
695
|
input.projectId,
|
|
682
696
|
];
|
|
683
697
|
let keyCreate;
|
|
698
|
+
let afterSpawn = null;
|
|
684
699
|
try {
|
|
685
|
-
|
|
700
|
+
try {
|
|
701
|
+
keyCreate = await runner.run(keyCreateCommand, {
|
|
702
|
+
cwd: stageDir,
|
|
703
|
+
cwdIdentity: { dev: prepared.dev, ino: prepared.ino },
|
|
704
|
+
cwdFd: prepared.fd,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
// The invocation threw (spawn/transport failure) after gcloud may or
|
|
709
|
+
// may not have written the staged key. Treat it as a lost result and
|
|
710
|
+
// poll the deterministic stage + current key list below instead of
|
|
711
|
+
// bubbling an unexpected error; the thrown text is never forwarded.
|
|
712
|
+
keyCreate = { status: "failed", code: null, stdout: "", stderr: "" };
|
|
713
|
+
}
|
|
714
|
+
// Keep the prepared directory descriptor open through this path check:
|
|
715
|
+
// otherwise an unlinked inode could be reused before dev/ino comparison.
|
|
716
|
+
afterSpawn = verifyStageDirIdentityAfterSpawn(stageDir, prepared);
|
|
686
717
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
718
|
+
finally {
|
|
719
|
+
closePreparedStageDir(prepared.fd);
|
|
720
|
+
}
|
|
721
|
+
// Before the result is recorded or any staged output is trusted, a
|
|
722
|
+
// mismatch — or a vanished directory — fails closed with nothing from
|
|
723
|
+
// the replacement read, chmod'ed, installed, or cleaned up, and the
|
|
724
|
+
// `key_create_started` checkpoint is retained for a reconcile-only resume.
|
|
725
|
+
if (afterSpawn !== null) {
|
|
726
|
+
return { status: "error", error: afterSpawn };
|
|
693
727
|
}
|
|
694
728
|
executed.push({
|
|
695
729
|
kind: "gcloud",
|
|
@@ -739,8 +773,7 @@ function uncertainResult(message) {
|
|
|
739
773
|
/** Owner-only permission of the key staging directory. */
|
|
740
774
|
const KEY_STAGE_DIR_MODE = 0o700;
|
|
741
775
|
/**
|
|
742
|
-
* Creates the deterministic staging directory
|
|
743
|
-
* identity.
|
|
776
|
+
* Creates or secures the deterministic staging directory and pins it open.
|
|
744
777
|
*
|
|
745
778
|
* A pre-existing entry is accepted only when it is a real directory (a
|
|
746
779
|
* crash between the checkpoint write and the create leaves an empty owned
|
|
@@ -750,10 +783,9 @@ const KEY_STAGE_DIR_MODE = 0o700;
|
|
|
750
783
|
* platform supports it (the mode is applied and verified on the open
|
|
751
784
|
* descriptor, never via a check-then-chmod path race), and any unsafe
|
|
752
785
|
* type/permission failure rejects the run without touching foreign
|
|
753
|
-
* entries.
|
|
754
|
-
*
|
|
755
|
-
* pre-existing-directory
|
|
756
|
-
* from the one fresh create).
|
|
786
|
+
* entries. Its open descriptor pins the directory object through process
|
|
787
|
+
* launch, preventing inode reuse from making a replacement appear identical.
|
|
788
|
+
* Exported so tests can exercise the pre-existing-directory path directly.
|
|
757
789
|
*/
|
|
758
790
|
export function prepareStageDir(keyPath, keyMarker) {
|
|
759
791
|
const stageDir = keyStageDir(keyPath, keyMarker);
|
|
@@ -767,33 +799,19 @@ export function prepareStageDir(keyPath, keyMarker) {
|
|
|
767
799
|
error: errorResult(SETUP_ERROR_CODES.KEY_CREATE_FAILED, `could not create a private staging directory for the service-account key: ${messageOf(error)}`),
|
|
768
800
|
};
|
|
769
801
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
802
|
+
}
|
|
803
|
+
const secured = secureExistingStageDir(stageDir, defaultKeyCleanupFs, "staging directory");
|
|
804
|
+
return secured.status === "ok"
|
|
805
|
+
? { status: "ok", dev: secured.dev, ino: secured.ino, fd: secured.fd }
|
|
806
|
+
: { status: "error", error: secured.error };
|
|
807
|
+
}
|
|
808
|
+
/** Closes the pinned staging descriptor after the subprocess identity check. */
|
|
809
|
+
function closePreparedStageDir(fd) {
|
|
778
810
|
try {
|
|
779
|
-
|
|
780
|
-
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
781
|
-
// The directory we just created was replaced before it could be
|
|
782
|
-
// verified: fail closed and never touch the replacement.
|
|
783
|
-
return {
|
|
784
|
-
status: "error",
|
|
785
|
-
error: errorResult(SETUP_ERROR_CODES.KEY_CREATE_FAILED, `the staging directory ${stageDir} is not a plain directory; remove it after ` +
|
|
786
|
-
`confirming it is not needed and rerun setup`),
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
return { status: "ok", dev: stat.dev, ino: stat.ino };
|
|
811
|
+
closeSync(fd);
|
|
790
812
|
}
|
|
791
|
-
catch
|
|
792
|
-
|
|
793
|
-
status: "error",
|
|
794
|
-
error: errorResult(SETUP_ERROR_CODES.KEY_CREATE_FAILED, `could not verify the staging directory ${stageDir}: ${messageOf(error)}; remove it ` +
|
|
795
|
-
`after confirming it is not needed and rerun setup`),
|
|
796
|
-
};
|
|
813
|
+
catch {
|
|
814
|
+
// Preserve the setup result; the descriptor is no longer in use.
|
|
797
815
|
}
|
|
798
816
|
}
|
|
799
817
|
/**
|
|
@@ -803,12 +821,14 @@ export function prepareStageDir(keyPath, keyMarker) {
|
|
|
803
821
|
* The directory is opened WITHOUT following symlinks and its type,
|
|
804
822
|
* device/inode identity, and owner-only mode are verified THROUGH the
|
|
805
823
|
* descriptor against the identity captured by `prepareStageDir`; only an
|
|
806
|
-
* exact match is allowed to receive an `fchmod` (the directory is ours)
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
824
|
+
* exact match is allowed to receive an `fchmod` (the directory is ours).
|
|
825
|
+
* The pinned prepared descriptor is inherited by the wrapper, which
|
|
826
|
+
* compares `statSync('.')` with `fstatSync(3)` before gcloud runs; a path
|
|
827
|
+
* replacement cannot pass merely by reusing the inode number. The
|
|
828
|
+
* post-spawn identity re-check stays as defense in depth. A replacement
|
|
829
|
+
* — symlink, non-directory, foreign directory, or mode failure — fails
|
|
830
|
+
* closed BEFORE the spawn and is never chmod'ed, read, or written
|
|
831
|
+
* through.
|
|
812
832
|
*/
|
|
813
833
|
function verifyStageDirBeforeSubprocess(stageDir, expected) {
|
|
814
834
|
let fd;
|
|
@@ -855,20 +875,58 @@ function verifyStageDirBeforeSubprocess(stageDir, expected) {
|
|
|
855
875
|
}
|
|
856
876
|
}
|
|
857
877
|
/**
|
|
858
|
-
*
|
|
878
|
+
* Re-checks the staging directory identity immediately AFTER the gcloud
|
|
879
|
+
* key-create subprocess returns, before its result is recorded or any
|
|
880
|
+
* staged output is trusted (defense in depth behind the wrapper's
|
|
881
|
+
* object-bound CWD, which already prevents the write from landing in a
|
|
882
|
+
* replacement).
|
|
883
|
+
*
|
|
884
|
+
* Only the exact device/inode captured by `prepareStageDir` passes;
|
|
885
|
+
* anything else fails closed with `key_create_failed` and nothing from
|
|
886
|
+
* the replacement is ever read, chmod'ed, installed, or cleaned up.
|
|
887
|
+
* Deliberately chmod-free (unlike the pre-spawn check): the entry may be
|
|
888
|
+
* foreign, so its mode is never touched. A transient swap-back — the
|
|
889
|
+
* replacement removed before this check — leaves no key in the real
|
|
890
|
+
* directory, so the bounded settlement ends `uncertain` without ever
|
|
891
|
+
* trusting the foreign directory.
|
|
892
|
+
*/
|
|
893
|
+
function verifyStageDirIdentityAfterSpawn(stageDir, expected) {
|
|
894
|
+
let lst;
|
|
895
|
+
try {
|
|
896
|
+
lst = lstatSync(stageDir);
|
|
897
|
+
}
|
|
898
|
+
catch (error) {
|
|
899
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
900
|
+
return errorResult(SETUP_ERROR_CODES.KEY_CREATE_FAILED, `the staging directory ${stageDir} disappeared while the key was being created; ` +
|
|
901
|
+
`nothing was trusted — rerun setup`);
|
|
902
|
+
}
|
|
903
|
+
return errorResult(SETUP_ERROR_CODES.KEY_CREATE_FAILED, `could not re-verify the staging directory ${stageDir}: ${messageOf(error)}; nothing ` +
|
|
904
|
+
`was trusted — rerun setup`);
|
|
905
|
+
}
|
|
906
|
+
if (lst === undefined || lst.isSymbolicLink() || !lst.isDirectory()) {
|
|
907
|
+
return errorResult(SETUP_ERROR_CODES.KEY_CREATE_FAILED, `the staging directory ${stageDir} is not a plain directory; nothing was trusted — ` +
|
|
908
|
+
`remove it after confirming it is not needed and rerun setup`);
|
|
909
|
+
}
|
|
910
|
+
if (lst.dev !== expected.dev || lst.ino !== expected.ino) {
|
|
911
|
+
return errorResult(SETUP_ERROR_CODES.KEY_CREATE_FAILED, `the staging directory ${stageDir} was replaced while the key was being created; ` +
|
|
912
|
+
`nothing was trusted — remove the replacement after confirming it is not needed and rerun setup`);
|
|
913
|
+
}
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Verifies and secures a deterministic directory through a no-follow fd.
|
|
859
918
|
*
|
|
860
919
|
* The entry must be a plain directory (a symlink or non-directory is
|
|
861
|
-
* refused and left untouched).
|
|
862
|
-
*
|
|
863
|
-
* and
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
* the directory are never touched. `label` names the directory kind in
|
|
868
|
-
* messages ("staging directory" or "cleanup directory").
|
|
920
|
+
* refused and left untouched). Descriptor and pathname identities are
|
|
921
|
+
* matched before any mode change; the mode is enforced through `fchmod`
|
|
922
|
+
* and verified on the descriptor. Success returns the still-open fd so a
|
|
923
|
+
* caller can pin the object across process launch; other callers close it
|
|
924
|
+
* after this check. Entries inside the directory are never touched.
|
|
925
|
+
* `label` names the directory kind in messages.
|
|
869
926
|
*/
|
|
870
927
|
function secureExistingStageDir(dirPath, fs, label) {
|
|
871
928
|
let fd;
|
|
929
|
+
let keepOpen = false;
|
|
872
930
|
try {
|
|
873
931
|
fd = fs.openSync(dirPath, constants.O_RDONLY | noFollowFlag() | directoryFlag());
|
|
874
932
|
}
|
|
@@ -888,11 +946,16 @@ function secureExistingStageDir(dirPath, fs, label) {
|
|
|
888
946
|
}
|
|
889
947
|
try {
|
|
890
948
|
let stat = fs.fstatSync(fd);
|
|
891
|
-
|
|
949
|
+
const pathStat = fs.lstatSync(dirPath);
|
|
950
|
+
if (!stat.isDirectory() ||
|
|
951
|
+
pathStat.isSymbolicLink() ||
|
|
952
|
+
!pathStat.isDirectory() ||
|
|
953
|
+
pathStat.dev !== stat.dev ||
|
|
954
|
+
pathStat.ino !== stat.ino) {
|
|
892
955
|
return {
|
|
893
956
|
status: "error",
|
|
894
|
-
error: errorResult(SETUP_ERROR_CODES.KEY_CREATE_FAILED, `the ${label} ${dirPath} is not
|
|
895
|
-
`is not needed and rerun setup`),
|
|
957
|
+
error: errorResult(SETUP_ERROR_CODES.KEY_CREATE_FAILED, `the ${label} ${dirPath} is not the verified plain directory; remove it after confirming ` +
|
|
958
|
+
`it is not needed and rerun setup`),
|
|
896
959
|
};
|
|
897
960
|
}
|
|
898
961
|
if ((Number(stat.mode) & 0o777) !== KEY_STAGE_DIR_MODE) {
|
|
@@ -914,7 +977,8 @@ function secureExistingStageDir(dirPath, fs, label) {
|
|
|
914
977
|
};
|
|
915
978
|
}
|
|
916
979
|
}
|
|
917
|
-
|
|
980
|
+
keepOpen = true;
|
|
981
|
+
return { status: "ok", dev: stat.dev, ino: stat.ino, fd };
|
|
918
982
|
}
|
|
919
983
|
catch (error) {
|
|
920
984
|
return {
|
|
@@ -923,11 +987,13 @@ function secureExistingStageDir(dirPath, fs, label) {
|
|
|
923
987
|
};
|
|
924
988
|
}
|
|
925
989
|
finally {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
990
|
+
if (!keepOpen) {
|
|
991
|
+
try {
|
|
992
|
+
fs.closeSync(fd);
|
|
993
|
+
}
|
|
994
|
+
catch {
|
|
995
|
+
// The security verdict is the one to report.
|
|
996
|
+
}
|
|
931
997
|
}
|
|
932
998
|
}
|
|
933
999
|
}
|
|
@@ -975,6 +1041,12 @@ function inspectStagedKeyParent(keyPath, keyMarker) {
|
|
|
975
1041
|
if (secured.status === "error") {
|
|
976
1042
|
return { status: "error", error: secured.error };
|
|
977
1043
|
}
|
|
1044
|
+
try {
|
|
1045
|
+
defaultKeyCleanupFs.closeSync(secured.fd);
|
|
1046
|
+
}
|
|
1047
|
+
catch {
|
|
1048
|
+
// Keep the validation result; the descriptor is no longer needed here.
|
|
1049
|
+
}
|
|
978
1050
|
return { status: "present", dev: secured.dev, ino: secured.ino };
|
|
979
1051
|
}
|
|
980
1052
|
/**
|
|
@@ -1052,35 +1124,42 @@ const defaultKeyCleanupFs = {
|
|
|
1052
1124
|
closeSync,
|
|
1053
1125
|
};
|
|
1054
1126
|
/**
|
|
1055
|
-
*
|
|
1056
|
-
* installation.
|
|
1057
|
-
*
|
|
1058
|
-
* The final key path is verified with lstat (never following a symlink) and
|
|
1059
|
-
* must be a regular file; a final symlink is refused and the staged
|
|
1060
|
-
* credential is retained. Before any deletion the deterministic stage
|
|
1061
|
-
* directory is atomically quarantined: renamed to the deterministic private
|
|
1062
|
-
* cleanup sibling path derived from the SAME persisted key marker (never an
|
|
1063
|
-
* unrecoverable random name), with the device/inode captured before the
|
|
1064
|
-
* rename re-verified after it — a source replaced between the capture and
|
|
1065
|
-
* the rename fails closed and nothing is deleted. Inside the quarantined
|
|
1066
|
-
* 0700 directory the staged entry is re-checked as a regular file with the
|
|
1067
|
-
* same device/inode as the no-follow final key immediately before its
|
|
1068
|
-
* unlink; any mismatch, foreign, or non-empty entry is preserved (the
|
|
1069
|
-
* directory is moved back to the stage path so the user finds the state
|
|
1070
|
-
* where the docs say it is). The empty quarantined directory is then
|
|
1071
|
-
* rmdir'd. Never recursive-delete.
|
|
1127
|
+
* Re-reads both quarantine paths after `renameSync` reports ENOENT.
|
|
1072
1128
|
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1077
|
-
* finished (success); both existing at once fails closed. The setup lock is
|
|
1078
|
-
* held for the whole run — a second cooperating setup process cannot
|
|
1079
|
-
* acquire it concurrently, so no other run can be touching these
|
|
1080
|
-
* deterministic paths — and Node offers no literal unlinkat without a raw
|
|
1081
|
-
* libuv binding, so this quarantine + post-rename identity design is the
|
|
1082
|
-
* permitted boundary for the pathname unlink race.
|
|
1129
|
+
* Either entry may have appeared, vanished, or been replaced since the
|
|
1130
|
+
* pre-rename inspection, so the stale pair must not decide the outcome.
|
|
1131
|
+
* A non-ENOENT inspection failure fails closed; otherwise the fresh pair
|
|
1132
|
+
* is returned (an entry confirmed absent is `undefined`).
|
|
1083
1133
|
*/
|
|
1134
|
+
function reinspectStageAndCleanup(stageDir, cleanupDir, fs) {
|
|
1135
|
+
let stage;
|
|
1136
|
+
try {
|
|
1137
|
+
stage = fs.lstatSync(stageDir);
|
|
1138
|
+
}
|
|
1139
|
+
catch (error) {
|
|
1140
|
+
if (!isNodeError(error) || error.code !== "ENOENT") {
|
|
1141
|
+
return {
|
|
1142
|
+
status: "error",
|
|
1143
|
+
error: errorResult(SETUP_ERROR_CODES.KEY_CREATE_FAILED, `could not inspect the staging directory ${stageDir}: ${messageOf(error)}; rerun setup to finish`),
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
stage = undefined;
|
|
1147
|
+
}
|
|
1148
|
+
let cleanup;
|
|
1149
|
+
try {
|
|
1150
|
+
cleanup = fs.lstatSync(cleanupDir);
|
|
1151
|
+
}
|
|
1152
|
+
catch (error) {
|
|
1153
|
+
if (!isNodeError(error) || error.code !== "ENOENT") {
|
|
1154
|
+
return {
|
|
1155
|
+
status: "error",
|
|
1156
|
+
error: errorResult(SETUP_ERROR_CODES.KEY_CREATE_FAILED, `could not inspect the cleanup directory ${cleanupDir}: ${messageOf(error)}; rerun setup to finish`),
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
cleanup = undefined;
|
|
1160
|
+
}
|
|
1161
|
+
return { status: "ok", stage, cleanup };
|
|
1162
|
+
}
|
|
1084
1163
|
export function cleanupOwnedStage(keyPath, keyMarker, fs = defaultKeyCleanupFs) {
|
|
1085
1164
|
const stageDir = keyStageDir(keyPath, keyMarker);
|
|
1086
1165
|
const cleanupDir = keyCleanupDir(keyPath, keyMarker);
|
|
@@ -1148,10 +1227,34 @@ export function cleanupOwnedStage(keyPath, keyMarker, fs = defaultKeyCleanupFs)
|
|
|
1148
1227
|
}
|
|
1149
1228
|
catch (error) {
|
|
1150
1229
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
1151
|
-
// The
|
|
1152
|
-
//
|
|
1153
|
-
//
|
|
1230
|
+
// The rename lost a source-or-destination race (the stage vanished
|
|
1231
|
+
// or the cleanup parent changed under us): the pre-rename
|
|
1232
|
+
// `cleanupLst` is now stale evidence and must not decide the
|
|
1233
|
+
// outcome. Re-inspect BOTH paths fresh — only a freshly confirmed
|
|
1234
|
+
// absent/absent pair is already-clean.
|
|
1235
|
+
const fresh = reinspectStageAndCleanup(stageDir, cleanupDir, fs);
|
|
1236
|
+
if (fresh.status === "error") {
|
|
1237
|
+
return fresh.error;
|
|
1238
|
+
}
|
|
1239
|
+
if (fresh.stage === undefined && fresh.cleanup === undefined) {
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
if (fresh.stage !== undefined && fresh.cleanup !== undefined) {
|
|
1243
|
+
return errorResult(SETUP_ERROR_CODES.KEY_CREATE_FAILED, `both the staging directory ${stageDir} and the cleanup directory ${cleanupDir} exist; ` +
|
|
1244
|
+
`remove one after confirming it is not needed and rerun setup`);
|
|
1245
|
+
}
|
|
1246
|
+
if (fresh.stage !== undefined) {
|
|
1247
|
+
// The stage is (still) there but the rename reported ENOENT, so
|
|
1248
|
+
// the quarantine never happened: fail closed and let the next
|
|
1249
|
+
// run retry it instead of deleting through a path we never
|
|
1250
|
+
// quarantined.
|
|
1251
|
+
return errorResult(SETUP_ERROR_CODES.KEY_CREATE_FAILED, `could not quarantine the staging directory ${stageDir}: the directory changed ` +
|
|
1252
|
+
`during the rename; rerun setup to finish`);
|
|
1253
|
+
}
|
|
1254
|
+
// Stage confirmed absent with a freshly present cleanup directory:
|
|
1255
|
+
// finish that directory below (it is re-validated there).
|
|
1154
1256
|
stageLst = undefined;
|
|
1257
|
+
cleanupLst = fresh.cleanup;
|
|
1155
1258
|
}
|
|
1156
1259
|
else {
|
|
1157
1260
|
return errorResult(SETUP_ERROR_CODES.KEY_CREATE_FAILED, `could not quarantine the staging directory ${stageDir}: ${messageOf(error)}; rerun setup to finish`);
|
|
@@ -1198,6 +1301,12 @@ export function cleanupOwnedStage(keyPath, keyMarker, fs = defaultKeyCleanupFs)
|
|
|
1198
1301
|
if (secured.status === "error") {
|
|
1199
1302
|
return secured.error;
|
|
1200
1303
|
}
|
|
1304
|
+
try {
|
|
1305
|
+
fs.closeSync(secured.fd);
|
|
1306
|
+
}
|
|
1307
|
+
catch {
|
|
1308
|
+
// Keep the validation result; the descriptor is no longer needed here.
|
|
1309
|
+
}
|
|
1201
1310
|
let entries;
|
|
1202
1311
|
try {
|
|
1203
1312
|
entries = fs.readdirSync(cleanupDir);
|