@stonyx/utils 0.2.3-alpha.31 → 0.2.3-alpha.32
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/file.js +36 -10
- package/package.json +1 -1
package/dist/file.js
CHANGED
|
@@ -70,14 +70,37 @@ export async function updateFile(filePath, data, options = {}) {
|
|
|
70
70
|
const swapFile = `${filePath}.temp-${process.pid}-${randomBytes(6).toString('base64url')}`;
|
|
71
71
|
// Which stage failed, so the catch can tell "the open collided" from "the
|
|
72
72
|
// open succeeded and something later failed" — see the catch.
|
|
73
|
-
let
|
|
73
|
+
let created = false;
|
|
74
74
|
try {
|
|
75
75
|
// `wx` turns any residual name collision into a loud EEXIST rather than a
|
|
76
76
|
// silent overwrite of another caller's swap bytes. `mode` here is masked
|
|
77
|
-
// by the umask, so it only narrows — the
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
// by the umask, so it only narrows — the fchmod below sets the exact bits.
|
|
78
|
+
//
|
|
79
|
+
// The open is deliberately separate from the write. Everything after this
|
|
80
|
+
// line addresses the *descriptor*, never the path again, so an attacker
|
|
81
|
+
// with write access to the directory cannot redirect it: unlinking the
|
|
82
|
+
// swap path and replacing it with a symlink after this point leaves the
|
|
83
|
+
// handle bound to the original inode. `fsp.chmod(swapFile, ...)` on the
|
|
84
|
+
// path did not have that property — it followed such a symlink and
|
|
85
|
+
// widened an arbitrary victim file with this process's privileges
|
|
86
|
+
// (abofs/stonyx-utils#45, Phase 3 HIGH-3).
|
|
87
|
+
const handle = await fsp.open(swapFile, 'wx', targetMode);
|
|
88
|
+
created = true;
|
|
89
|
+
try {
|
|
90
|
+
await handle.writeFile(options.json ? objToJson(data) : String(data), 'utf8');
|
|
91
|
+
// fchmod(2) on the descriptor, not chmod(2) on the path. Needed at all
|
|
92
|
+
// because the `mode` above is umask-masked and therefore only narrows:
|
|
93
|
+
// a 0666 target would come back 0644 under the usual 022.
|
|
94
|
+
await handle.chmod(targetMode);
|
|
95
|
+
}
|
|
96
|
+
catch (writeError) {
|
|
97
|
+
await handle.close().catch(() => { });
|
|
98
|
+
throw writeError;
|
|
99
|
+
}
|
|
100
|
+
// Not in a `finally`: a close failure is a write failure (the flush can
|
|
101
|
+
// land here), so it has to surface rather than be swallowed — which is
|
|
102
|
+
// what `fsp.writeFile` did when it owned this close.
|
|
103
|
+
await handle.close();
|
|
81
104
|
await fsp.rename(swapFile, filePath);
|
|
82
105
|
}
|
|
83
106
|
catch (swapError) {
|
|
@@ -88,11 +111,14 @@ export async function updateFile(filePath, data, options = {}) {
|
|
|
88
111
|
// #44. That is EEXIST *from the write stage* — both halves matter.
|
|
89
112
|
// - Code alone is not enough: `rename` surfaces EEXIST on Windows and
|
|
90
113
|
// some network filesystems, and that file is one we created.
|
|
91
|
-
// - Stage alone is not enough:
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
114
|
+
// - Stage alone is not enough: `rename` is the only stage after the
|
|
115
|
+
// open that can raise EEXIST at all, and by then the file is ours —
|
|
116
|
+
// so a stage flag that answered "did the write finish?" would send
|
|
117
|
+
// an ENOSPC/EDQUOT/EIO mid-write down the skip path and reinstate
|
|
118
|
+
// the orphaned partial payload this PR exists to close. `created` is
|
|
119
|
+
// set the instant the `wx` open returns, which is the only reading
|
|
120
|
+
// of the stage that means "this call owns the path".
|
|
121
|
+
const lostTheOpenRace = !created && isNodeError(swapError) && swapError.code === 'EEXIST';
|
|
96
122
|
if (!lostTheOpenRace) {
|
|
97
123
|
await fsp.unlink(swapFile).catch(() => { });
|
|
98
124
|
}
|