release-skill 0.2.0 → 0.2.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +10 -0
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +14 -0
- package/INSTALL.md +24 -4
- package/INSTALL.zh-CN.md +22 -4
- package/README.md +15 -13
- package/README.zh-CN.md +10 -13
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +1 -1
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +1 -1
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +1 -1
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +10 -0
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +85363 -0
- package/adapters/workbuddy/bin/release-skill.mjs +54 -0
- package/adapters/workbuddy/native/safe-write/binding.gyp +41 -0
- package/adapters/workbuddy/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/adapters/workbuddy/native/safe-write/prebuilds.json +24 -0
- package/adapters/workbuddy/native/safe-write/src/safe_write.cc +2032 -0
- package/adapters/workbuddy/schemas/.render-manifest.json +37 -0
- package/adapters/workbuddy/schemas/approval-record.schema.json +115 -0
- package/adapters/workbuddy/schemas/artifact-lock.schema.json +111 -0
- package/adapters/workbuddy/schemas/artifact-plan.schema.json +52 -0
- package/adapters/workbuddy/schemas/artifact-policy.schema.json +76 -0
- package/adapters/workbuddy/schemas/evidence-event.schema.json +89 -0
- package/adapters/workbuddy/schemas/release-plan.schema.json +882 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +909 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +343 -0
- package/adapters/workbuddy/skills/release-assess/SKILL.md +51 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +77 -0
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +92 -0
- package/adapters/workbuddy/skills/release-publish/SKILL.md +57 -0
- package/adapters/workbuddy/skills/release-reconcile/SKILL.md +73 -0
- package/adapters/workbuddy/skills/release-setup/SKILL.md +95 -0
- package/adapters/workbuddy/skills/release-verify/SKILL.md +70 -0
- package/bin/release-skill.bundle.mjs +1 -1
- package/package.json +2 -1
- package/references/06-adapter-contract.md +1 -1
- package/scripts/sync-public-files.mjs +21 -2
- package/src/producers/build-adapters.mjs +38 -4
|
@@ -0,0 +1,2032 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-API safe write addon — identity-bound / fd-relative primitives.
|
|
3
|
+
*
|
|
4
|
+
* All directory traversal uses openat() with O_NOFOLLOW | O_CLOEXEC.
|
|
5
|
+
* openRoot walks from "/" one segment at a time to prevent ancestor-symlink
|
|
6
|
+
* attacks. readFile opens via openat(O_RDONLY|O_NONBLOCK|O_NOFOLLOW|O_CLOEXEC)
|
|
7
|
+
* to prevent FIFO blocking, validates via fstat on the fd (regular, nlink=1),
|
|
8
|
+
* reads all bytes, then performs post-read identity verification.
|
|
9
|
+
*
|
|
10
|
+
* createTemp validates Buffer data and mode (0..0777), writes data with
|
|
11
|
+
* EINTR/partial-write handling, applies fchmod for exact mode, and returns
|
|
12
|
+
* a TempToken bound to the open temp fd, dup'd directory fd, creation-time
|
|
13
|
+
* dev/ino, temp name, and active/committed state. The TempToken object is
|
|
14
|
+
* tagged with napi_type_tag for identity verification.
|
|
15
|
+
*
|
|
16
|
+
* rename verifies temp-file identity via the token's fd, then executes an
|
|
17
|
+
* atomic no-replace rename, verifies target identity (dev/ino/nlink/type),
|
|
18
|
+
* fsyncs the directory, and consumes the token.
|
|
19
|
+
*
|
|
20
|
+
* abortTemp only unlinks when identity matches (dev/ino/type + nlink==1).
|
|
21
|
+
* chmod uses fd-relative openat + fchmod (not path-based fchmodat).
|
|
22
|
+
*
|
|
23
|
+
* All error messages expose only syscall, errno, and safe segment —
|
|
24
|
+
* never absolute paths.
|
|
25
|
+
*
|
|
26
|
+
* @file native/safe-write/src/safe_write.cc
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// Platform feature macros must come before all other includes.
|
|
30
|
+
#if defined(__APPLE__)
|
|
31
|
+
# ifndef _DARWIN_C_SOURCE
|
|
32
|
+
# define _DARWIN_C_SOURCE
|
|
33
|
+
# endif
|
|
34
|
+
#endif
|
|
35
|
+
|
|
36
|
+
#include <node_api.h>
|
|
37
|
+
|
|
38
|
+
#include <cerrno>
|
|
39
|
+
#include <climits>
|
|
40
|
+
#include <cstdio>
|
|
41
|
+
#include <cstdlib>
|
|
42
|
+
#include <cstring>
|
|
43
|
+
#include <new>
|
|
44
|
+
#include <string>
|
|
45
|
+
#include <vector>
|
|
46
|
+
|
|
47
|
+
#include <fcntl.h>
|
|
48
|
+
#include <sys/stat.h>
|
|
49
|
+
#include <unistd.h>
|
|
50
|
+
|
|
51
|
+
#if defined(__linux__)
|
|
52
|
+
# include <sys/syscall.h>
|
|
53
|
+
#endif
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Platform-specific timespec accessors for st_mtim/st_ctim.
|
|
57
|
+
// macOS uses st_mtimespec/st_ctimespec; Linux uses st_mtim/st_ctim.
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
#if defined(__APPLE__)
|
|
61
|
+
# define ST_MTIM_NS(st) \
|
|
62
|
+
((int64_t)(st).st_mtimespec.tv_sec * 1000000000LL + (st).st_mtimespec.tv_nsec)
|
|
63
|
+
# define ST_CTIM_NS(st) \
|
|
64
|
+
((int64_t)(st).st_ctimespec.tv_sec * 1000000000LL + (st).st_ctimespec.tv_nsec)
|
|
65
|
+
#elif defined(__linux__)
|
|
66
|
+
# define ST_MTIM_NS(st) \
|
|
67
|
+
((int64_t)(st).st_mtim.tv_sec * 1000000000LL + (st).st_mtim.tv_nsec)
|
|
68
|
+
# define ST_CTIM_NS(st) \
|
|
69
|
+
((int64_t)(st).st_ctim.tv_sec * 1000000000LL + (st).st_ctim.tv_nsec)
|
|
70
|
+
#else
|
|
71
|
+
# define ST_MTIM_NS(st) ((int64_t)(st).st_mtime * 1000000000LL)
|
|
72
|
+
# define ST_CTIM_NS(st) ((int64_t)(st).st_ctime * 1000000000LL)
|
|
73
|
+
#endif
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Type tags for N-API object identity verification.
|
|
77
|
+
// Random 64-bit values baked into the binary to prevent cross-addon forgery.
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
static constexpr uint64_t kDirHandleTag = 0x8a3f1b2c4d5e6f70ULL;
|
|
81
|
+
static constexpr uint64_t kTempTokenTag = 0xd4e5f6a7b8c90123ULL;
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Test seam for deterministic N-API failure injection.
|
|
85
|
+
// Only available in test builds (SAFE_WRITE_TEST_SEAM=1).
|
|
86
|
+
// Production builds never define this, so the test hook does not exist.
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
#ifdef SAFE_WRITE_TEST_SEAM
|
|
90
|
+
#include <unordered_map>
|
|
91
|
+
#include <mutex>
|
|
92
|
+
|
|
93
|
+
// Test seam state: maps failure point names to failure counts.
|
|
94
|
+
// A count of -1 means "fail on every call".
|
|
95
|
+
static std::mutex g_test_seam_mutex;
|
|
96
|
+
static std::unordered_map<std::string, int> g_test_seam_failures;
|
|
97
|
+
|
|
98
|
+
// Test seam statistics: tracks calls and failures for each failure point.
|
|
99
|
+
static std::unordered_map<std::string, int> g_test_seam_calls;
|
|
100
|
+
static std::unordered_map<std::string, int> g_test_seam_failed;
|
|
101
|
+
|
|
102
|
+
// Check if a test seam failure should be triggered.
|
|
103
|
+
// Returns true if the failure should be injected.
|
|
104
|
+
static bool TestSeamShouldFail(const char* point) {
|
|
105
|
+
std::lock_guard<std::mutex> lock(g_test_seam_mutex);
|
|
106
|
+
g_test_seam_calls[point]++;
|
|
107
|
+
auto it = g_test_seam_failures.find(point);
|
|
108
|
+
if (it != g_test_seam_failures.end()) {
|
|
109
|
+
if (it->second == -1) {
|
|
110
|
+
// Fail every time
|
|
111
|
+
g_test_seam_failed[point]++;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (it->second > 0) {
|
|
115
|
+
// Fail N times
|
|
116
|
+
it->second--;
|
|
117
|
+
g_test_seam_failed[point]++;
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Test seam failure point names for CreateTemp N-API calls.
|
|
125
|
+
static const char* kSeamCreateObject = "createTemp:napi_create_object";
|
|
126
|
+
static const char* kSeamCreateString = "createTemp:napi_create_string_utf8";
|
|
127
|
+
static const char* kSeamCreateExternal = "createTemp:napi_create_external";
|
|
128
|
+
static const char* kSeamSetProperty = "createTemp:napi_set_named_property";
|
|
129
|
+
static const char* kSeamTypeTag = "createTemp:napi_type_tag_object";
|
|
130
|
+
|
|
131
|
+
// Test seam failure point names for other functions.
|
|
132
|
+
static const char* kSeamOpenRootCreateObject = "openRoot:napi_create_object";
|
|
133
|
+
static const char* kSeamOpenDirCreateObject = "openDir:napi_create_object";
|
|
134
|
+
static const char* kSeamReadFileCreateObject = "readFile:napi_create_object";
|
|
135
|
+
static const char* kSeamAbortTempCreateObject = "abortTemp:napi_create_object";
|
|
136
|
+
static const char* kSeamAbortUnlink = "abortTemp:unlinkat";
|
|
137
|
+
|
|
138
|
+
// Reset all test seam state.
|
|
139
|
+
static void TestSeamReset() {
|
|
140
|
+
std::lock_guard<std::mutex> lock(g_test_seam_mutex);
|
|
141
|
+
g_test_seam_failures.clear();
|
|
142
|
+
g_test_seam_calls.clear();
|
|
143
|
+
g_test_seam_failed.clear();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Configure a test seam failure point.
|
|
147
|
+
// count: -1 = fail every time, 0 = don't fail, N = fail N times.
|
|
148
|
+
static void TestSeamConfigure(const char* point, int count) {
|
|
149
|
+
std::lock_guard<std::mutex> lock(g_test_seam_mutex);
|
|
150
|
+
g_test_seam_failures[point] = count;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Get the number of times a failure point was triggered.
|
|
154
|
+
static int TestSeamGetFailedCount(const char* point) {
|
|
155
|
+
std::lock_guard<std::mutex> lock(g_test_seam_mutex);
|
|
156
|
+
return g_test_seam_failed[point];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Get the number of times a failure point was called.
|
|
160
|
+
static int TestSeamGetCallCount(const char* point) {
|
|
161
|
+
std::lock_guard<std::mutex> lock(g_test_seam_mutex);
|
|
162
|
+
return g_test_seam_calls[point];
|
|
163
|
+
}
|
|
164
|
+
#endif // SAFE_WRITE_TEST_SEAM
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// N-API status check macro
|
|
168
|
+
//
|
|
169
|
+
// NAPI_CHECK — for napi_value-returning functions; returns nullptr on failure.
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
#define NAPI_CHECK(call) \
|
|
173
|
+
do { \
|
|
174
|
+
napi_status _s = (call); \
|
|
175
|
+
if (_s != napi_ok) { \
|
|
176
|
+
napi_throw_error(env, nullptr, "N-API call failed: " #call); \
|
|
177
|
+
return nullptr; \
|
|
178
|
+
} \
|
|
179
|
+
} while (0)
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Platform-specific atomic no-replace rename
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
#if defined(__APPLE__)
|
|
186
|
+
# include <sys/errno.h>
|
|
187
|
+
# ifndef RENAME_EXCL
|
|
188
|
+
# define RENAME_EXCL 0x00000001
|
|
189
|
+
# endif
|
|
190
|
+
static int safe_rename_noreplace(int src_dirfd, const char* src_name,
|
|
191
|
+
int dst_dirfd, const char* dst_name) {
|
|
192
|
+
return renameatx_np(src_dirfd, src_name, dst_dirfd, dst_name, RENAME_EXCL);
|
|
193
|
+
}
|
|
194
|
+
static int safe_rename_exchange(int src_dirfd, const char* src_name,
|
|
195
|
+
int dst_dirfd, const char* dst_name) {
|
|
196
|
+
return renameatx_np(src_dirfd, src_name, dst_dirfd, dst_name, RENAME_SWAP);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#elif defined(__linux__)
|
|
200
|
+
# ifndef RENAME_NOREPLACE
|
|
201
|
+
# define RENAME_NOREPLACE (1 << 0)
|
|
202
|
+
# endif
|
|
203
|
+
# ifndef RENAME_EXCHANGE
|
|
204
|
+
# define RENAME_EXCHANGE (1 << 1)
|
|
205
|
+
# endif
|
|
206
|
+
static int safe_rename_noreplace(int src_dirfd, const char* src_name,
|
|
207
|
+
int dst_dirfd, const char* dst_name) {
|
|
208
|
+
return static_cast<int>(
|
|
209
|
+
syscall(SYS_renameat2, src_dirfd, src_name, dst_dirfd, dst_name,
|
|
210
|
+
static_cast<unsigned>(RENAME_NOREPLACE)));
|
|
211
|
+
}
|
|
212
|
+
static int safe_rename_exchange(int src_dirfd, const char* src_name,
|
|
213
|
+
int dst_dirfd, const char* dst_name) {
|
|
214
|
+
return static_cast<int>(
|
|
215
|
+
syscall(SYS_renameat2, src_dirfd, src_name, dst_dirfd, dst_name,
|
|
216
|
+
static_cast<unsigned>(RENAME_EXCHANGE)));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#else
|
|
220
|
+
static int safe_rename_noreplace(int /*src_dirfd*/, const char* /*src_name*/,
|
|
221
|
+
int /*dst_dirfd*/, const char* /*dst_name*/) {
|
|
222
|
+
errno = ENOSYS;
|
|
223
|
+
return -1;
|
|
224
|
+
}
|
|
225
|
+
static int safe_rename_exchange(int /*src_dirfd*/, const char* /*src_name*/,
|
|
226
|
+
int /*dst_dirfd*/, const char* /*dst_name*/) {
|
|
227
|
+
errno = ENOSYS;
|
|
228
|
+
return -1;
|
|
229
|
+
}
|
|
230
|
+
#endif
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Helpers
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
static napi_value ThrowCode(napi_env env, const char* code, const char* msg) {
|
|
237
|
+
napi_value code_val, msg_val;
|
|
238
|
+
napi_create_string_utf8(env, code, NAPI_AUTO_LENGTH, &code_val);
|
|
239
|
+
napi_create_string_utf8(env, msg, NAPI_AUTO_LENGTH, &msg_val);
|
|
240
|
+
napi_value error;
|
|
241
|
+
napi_create_error(env, code_val, msg_val, &error);
|
|
242
|
+
napi_throw(env, error);
|
|
243
|
+
return nullptr;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
static napi_value ThrowErrno(napi_env env, const char* syscall, int err,
|
|
247
|
+
const char* segment) {
|
|
248
|
+
char buf[512];
|
|
249
|
+
snprintf(buf, sizeof(buf), "%s: errno=%d segment=%s", syscall, err, segment);
|
|
250
|
+
return ThrowCode(env, "PATH_UNSAFE", buf);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
static bool IsSafeSegment(const char* s, size_t len) {
|
|
254
|
+
if (len == 0) return false;
|
|
255
|
+
if (s[0] == '.' && (len == 1 || (len == 2 && s[1] == '.'))) return false;
|
|
256
|
+
for (size_t i = 0; i < len; ++i) {
|
|
257
|
+
if (s[i] == '/' || s[i] == '\0') return false;
|
|
258
|
+
}
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Segment validation that allows "." for directory self-query (e.g. readEntry(".")),
|
|
263
|
+
* but still rejects "..", slashes, and NUL. */
|
|
264
|
+
static bool IsSafeSegmentAllowDot(const char* s, size_t len) {
|
|
265
|
+
if (len == 0) return false;
|
|
266
|
+
if (len == 1 && s[0] == '.') return true; // Allow "."
|
|
267
|
+
if (s[0] == '.' && len == 2 && s[1] == '.') return false; // Reject ".."
|
|
268
|
+
for (size_t i = 0; i < len; ++i) {
|
|
269
|
+
if (s[i] == '/' || s[i] == '\0') return false;
|
|
270
|
+
}
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Close fd without EINTR retry. Per POSIX, close() behavior on EINTR is
|
|
275
|
+
* undefined — the fd may or may not be closed. Retrying risks closing a
|
|
276
|
+
* reused fd. CLOEXEC handles cleanup at process exit. */
|
|
277
|
+
static int SafeClose(int fd) {
|
|
278
|
+
return ::close(fd);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** UTF-8 extraction with safe NUL-terminated buffer.
|
|
282
|
+
* Returns false and throws on N-API failure; caller must return immediately. */
|
|
283
|
+
static bool GetUtf8(napi_env env, napi_value val, std::string& out) {
|
|
284
|
+
size_t len = 0;
|
|
285
|
+
if (napi_get_value_string_utf8(env, val, nullptr, 0, &len) != napi_ok) {
|
|
286
|
+
napi_throw_error(env, nullptr, "N-API call failed: napi_get_value_string_utf8");
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
std::vector<char> buf(len + 1, '\0');
|
|
290
|
+
size_t written = 0;
|
|
291
|
+
if (napi_get_value_string_utf8(env, val, buf.data(), len + 1, &written) !=
|
|
292
|
+
napi_ok) {
|
|
293
|
+
napi_throw_error(env, nullptr, "N-API call failed: napi_get_value_string_utf8");
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
out = std::string(buf.data(), written);
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Extract int32 from N-API value.
|
|
301
|
+
* Returns false and throws on N-API failure; caller must return immediately. */
|
|
302
|
+
static bool GetInt32(napi_env env, napi_value val, int32_t& out) {
|
|
303
|
+
if (napi_get_value_int32(env, val, &out) != napi_ok) {
|
|
304
|
+
napi_throw_error(env, nullptr, "N-API call failed: napi_get_value_int32");
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
static bool GetNamedInt64(napi_env env, napi_value obj, const char* name,
|
|
311
|
+
int64_t& out) {
|
|
312
|
+
napi_value value;
|
|
313
|
+
if (napi_get_named_property(env, obj, name, &value) != napi_ok) return false;
|
|
314
|
+
return napi_get_value_int64(env, value, &out) == napi_ok;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
static bool GetNamedDouble(napi_env env, napi_value obj, const char* name,
|
|
318
|
+
double& out) {
|
|
319
|
+
napi_value value;
|
|
320
|
+
if (napi_get_named_property(env, obj, name, &value) != napi_ok) return false;
|
|
321
|
+
return napi_get_value_double(env, value, &out) == napi_ok;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
struct ExpectedIdentity {
|
|
325
|
+
int64_t dev;
|
|
326
|
+
int64_t ino;
|
|
327
|
+
int64_t nlink;
|
|
328
|
+
int64_t size;
|
|
329
|
+
int64_t mode;
|
|
330
|
+
double mtime_ns;
|
|
331
|
+
double ctime_ns;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
static bool GetExpectedIdentity(napi_env env, napi_value value,
|
|
335
|
+
ExpectedIdentity& expected) {
|
|
336
|
+
napi_valuetype type;
|
|
337
|
+
if (napi_typeof(env, value, &type) != napi_ok || type != napi_object)
|
|
338
|
+
return false;
|
|
339
|
+
return GetNamedInt64(env, value, "dev", expected.dev) &&
|
|
340
|
+
GetNamedInt64(env, value, "ino", expected.ino) &&
|
|
341
|
+
GetNamedInt64(env, value, "nlink", expected.nlink) &&
|
|
342
|
+
GetNamedInt64(env, value, "size", expected.size) &&
|
|
343
|
+
GetNamedInt64(env, value, "mode", expected.mode) &&
|
|
344
|
+
GetNamedDouble(env, value, "mtimeNs", expected.mtime_ns) &&
|
|
345
|
+
GetNamedDouble(env, value, "ctimeNs", expected.ctime_ns);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
static bool StatMatchesExpected(const struct stat& st,
|
|
349
|
+
const ExpectedIdentity& expected) {
|
|
350
|
+
return S_ISREG(st.st_mode) &&
|
|
351
|
+
static_cast<int64_t>(st.st_dev) == expected.dev &&
|
|
352
|
+
static_cast<int64_t>(st.st_ino) == expected.ino &&
|
|
353
|
+
static_cast<int64_t>(st.st_nlink) == expected.nlink &&
|
|
354
|
+
static_cast<int64_t>(st.st_size) == expected.size &&
|
|
355
|
+
static_cast<int64_t>(st.st_mode & 07777) == expected.mode &&
|
|
356
|
+
static_cast<double>(ST_MTIM_NS(st)) == expected.mtime_ns &&
|
|
357
|
+
static_cast<double>(ST_CTIM_NS(st)) == expected.ctime_ns;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// A successful directory-entry rename may update ctime even though the file
|
|
361
|
+
// content and inode are unchanged. Post-exchange verification therefore keeps
|
|
362
|
+
// every stable identity/content field except ctime; mtime still detects an
|
|
363
|
+
// in-place content mutation.
|
|
364
|
+
static bool StatMatchesExpectedAfterRename(
|
|
365
|
+
const struct stat& st, const ExpectedIdentity& expected) {
|
|
366
|
+
return S_ISREG(st.st_mode) &&
|
|
367
|
+
static_cast<int64_t>(st.st_dev) == expected.dev &&
|
|
368
|
+
static_cast<int64_t>(st.st_ino) == expected.ino &&
|
|
369
|
+
static_cast<int64_t>(st.st_nlink) == expected.nlink &&
|
|
370
|
+
static_cast<int64_t>(st.st_size) == expected.size &&
|
|
371
|
+
static_cast<int64_t>(st.st_mode & 07777) == expected.mode &&
|
|
372
|
+
static_cast<double>(ST_MTIM_NS(st)) == expected.mtime_ns;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Get the fd from a JS handle object, verifying napi_type_tag.
|
|
376
|
+
* Returns -1 on failure (invalid handle), without throwing. */
|
|
377
|
+
static int GetDirFd(napi_env env, napi_value js_handle_obj) {
|
|
378
|
+
if (!js_handle_obj) return -1;
|
|
379
|
+
napi_valuetype t;
|
|
380
|
+
if (napi_typeof(env, js_handle_obj, &t) != napi_ok) return -1;
|
|
381
|
+
if (t != napi_object) return -1;
|
|
382
|
+
|
|
383
|
+
// Verify type tag — rejects objects from other addons or forged objects
|
|
384
|
+
bool tag_ok = false;
|
|
385
|
+
napi_type_tag check_tag = {kDirHandleTag, 0};
|
|
386
|
+
if (napi_check_object_type_tag(env, js_handle_obj, &check_tag, &tag_ok) !=
|
|
387
|
+
napi_ok)
|
|
388
|
+
return -1;
|
|
389
|
+
if (!tag_ok) return -1;
|
|
390
|
+
|
|
391
|
+
napi_value ext;
|
|
392
|
+
if (napi_get_named_property(env, js_handle_obj, "_handle", &ext) != napi_ok)
|
|
393
|
+
return -1;
|
|
394
|
+
void* data = nullptr;
|
|
395
|
+
if (napi_get_value_external(env, ext, &data) != napi_ok) return -1;
|
|
396
|
+
if (!data) return -1;
|
|
397
|
+
return *static_cast<int*>(data);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
// Segment extraction — argv[idx] must be a safe path segment string
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
struct SegmentArg {
|
|
405
|
+
std::string value;
|
|
406
|
+
bool ok;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
static SegmentArg GetSegment(napi_env env, napi_value* argv, size_t argc,
|
|
410
|
+
size_t idx, bool allow_dot = false) {
|
|
411
|
+
if (argc <= idx || !argv[idx]) return {"", false};
|
|
412
|
+
napi_valuetype t;
|
|
413
|
+
if (napi_typeof(env, argv[idx], &t) != napi_ok) return {"", false};
|
|
414
|
+
if (t != napi_string) return {"", false};
|
|
415
|
+
std::string s;
|
|
416
|
+
if (!GetUtf8(env, argv[idx], s)) return {"", false};
|
|
417
|
+
if (allow_dot ? !IsSafeSegmentAllowDot(s.c_str(), s.size())
|
|
418
|
+
: !IsSafeSegment(s.c_str(), s.size()))
|
|
419
|
+
return {"", false};
|
|
420
|
+
return {s, true};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// Directory handle — just a bare fd stored as an napi_external.
|
|
425
|
+
// The external's destructor closes the fd.
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
static void DirFdFinalize(napi_env /*env*/, void* data, void* /*hint*/) {
|
|
429
|
+
if (data) {
|
|
430
|
+
int fd = *static_cast<int*>(data);
|
|
431
|
+
if (fd >= 0) SafeClose(fd);
|
|
432
|
+
delete static_cast<int*>(data);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Create a JS object wrapping a bare int fd via napi_external.
|
|
437
|
+
* Tags the object with kDirHandleTag for type verification.
|
|
438
|
+
* On N-API failure after fd allocation, closes the fd to prevent leaks. */
|
|
439
|
+
static napi_value WrapDirFd(napi_env env, int fd) {
|
|
440
|
+
napi_value result, ext;
|
|
441
|
+
if (napi_create_object(env, &result) != napi_ok) {
|
|
442
|
+
SafeClose(fd);
|
|
443
|
+
napi_throw_error(env, nullptr, "N-API call failed: napi_create_object");
|
|
444
|
+
return nullptr;
|
|
445
|
+
}
|
|
446
|
+
int* heap_fd = new (std::nothrow) int(fd);
|
|
447
|
+
if (!heap_fd) {
|
|
448
|
+
SafeClose(fd);
|
|
449
|
+
napi_throw_error(env, nullptr, "allocation failed");
|
|
450
|
+
return nullptr;
|
|
451
|
+
}
|
|
452
|
+
if (napi_create_external(env, heap_fd, DirFdFinalize, nullptr, &ext) !=
|
|
453
|
+
napi_ok) {
|
|
454
|
+
// napi_create_external failed — heap_fd destructor doesn't close fd
|
|
455
|
+
SafeClose(fd);
|
|
456
|
+
delete heap_fd;
|
|
457
|
+
napi_throw_error(env, nullptr, "N-API call failed: napi_create_external");
|
|
458
|
+
return nullptr;
|
|
459
|
+
}
|
|
460
|
+
if (napi_set_named_property(env, result, "_handle", ext) != napi_ok) {
|
|
461
|
+
// ext destructor will call DirFdFinalize which closes fd
|
|
462
|
+
napi_throw_error(env, nullptr, "N-API call failed: napi_set_named_property");
|
|
463
|
+
return nullptr;
|
|
464
|
+
}
|
|
465
|
+
// Tag the object so only our addon's handles pass type check
|
|
466
|
+
napi_type_tag tag = {kDirHandleTag, 0};
|
|
467
|
+
if (napi_type_tag_object(env, result, &tag) != napi_ok) {
|
|
468
|
+
napi_throw_error(env, nullptr, "N-API call failed: napi_type_tag_object");
|
|
469
|
+
return nullptr;
|
|
470
|
+
}
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
// TempToken — identity-bound safe temp file token
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
struct TempToken {
|
|
479
|
+
int tmp_fd;
|
|
480
|
+
int dir_fd;
|
|
481
|
+
dev_t tmp_dev;
|
|
482
|
+
ino_t tmp_ino;
|
|
483
|
+
char name[PATH_MAX];
|
|
484
|
+
bool active;
|
|
485
|
+
bool committed;
|
|
486
|
+
|
|
487
|
+
TempToken()
|
|
488
|
+
: tmp_fd(-1), dir_fd(-1), tmp_dev(0), tmp_ino(0), active(true),
|
|
489
|
+
committed(false) {
|
|
490
|
+
name[0] = '\0';
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
~TempToken() { CloseFds(); }
|
|
494
|
+
|
|
495
|
+
void CloseFds() {
|
|
496
|
+
if (tmp_fd >= 0) {
|
|
497
|
+
SafeClose(tmp_fd);
|
|
498
|
+
tmp_fd = -1;
|
|
499
|
+
}
|
|
500
|
+
if (dir_fd >= 0) {
|
|
501
|
+
SafeClose(dir_fd);
|
|
502
|
+
dir_fd = -1;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
TempToken(const TempToken&) = delete;
|
|
507
|
+
TempToken& operator=(const TempToken&) = delete;
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// Forward declarations — defined in the identity-bound cleanup section below.
|
|
511
|
+
static bool identity_unlink(TempToken* tok);
|
|
512
|
+
static void cleanup_temp(TempToken* tok);
|
|
513
|
+
|
|
514
|
+
static void TempTokenFinalize(napi_env /*env*/, void* data, void* /*hint*/) {
|
|
515
|
+
TempToken* tok = static_cast<TempToken*>(data);
|
|
516
|
+
if (tok) {
|
|
517
|
+
// Identity-bound cleanup: only unlink if the token is still active
|
|
518
|
+
// (not consumed by rename/abortTemp or already cleaned up by a
|
|
519
|
+
// CreateTemp N-API failure path).
|
|
520
|
+
if (tok->active && !tok->committed) {
|
|
521
|
+
identity_unlink(tok);
|
|
522
|
+
}
|
|
523
|
+
delete tok;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Unwrap a TempToken from a JS object, verifying napi_type_tag.
|
|
528
|
+
* Returns nullptr if the object is not a genuine token from this addon.
|
|
529
|
+
* On N-API failure, returns nullptr (caller must check). */
|
|
530
|
+
static TempToken* GetTempToken(napi_env env, napi_value js_obj) {
|
|
531
|
+
if (!js_obj) return nullptr;
|
|
532
|
+
napi_valuetype t;
|
|
533
|
+
if (napi_typeof(env, js_obj, &t) != napi_ok) return nullptr;
|
|
534
|
+
if (t != napi_object) return nullptr;
|
|
535
|
+
|
|
536
|
+
// Verify type tag
|
|
537
|
+
bool tag_ok = false;
|
|
538
|
+
napi_type_tag check_tag = {kTempTokenTag, 0};
|
|
539
|
+
if (napi_check_object_type_tag(env, js_obj, &check_tag, &tag_ok) != napi_ok)
|
|
540
|
+
return nullptr;
|
|
541
|
+
if (!tag_ok) return nullptr;
|
|
542
|
+
|
|
543
|
+
napi_value tok_ext;
|
|
544
|
+
if (napi_get_named_property(env, js_obj, "_token", &tok_ext) != napi_ok)
|
|
545
|
+
return nullptr;
|
|
546
|
+
void* tok_data = nullptr;
|
|
547
|
+
if (napi_get_value_external(env, tok_ext, &tok_data) != napi_ok)
|
|
548
|
+
return nullptr;
|
|
549
|
+
return static_cast<TempToken*>(tok_data);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
// openRoot(path) -> { _handle }
|
|
554
|
+
//
|
|
555
|
+
// Walks from "/" one segment at a time with openat(O_DIRECTORY|O_NOFOLLOW|
|
|
556
|
+
// O_CLOEXEC). Rejects non-absolute paths, ancestor symlinks, ".", "..",
|
|
557
|
+
// and NUL bytes.
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
static napi_value OpenRoot(napi_env env, napi_callback_info info) {
|
|
561
|
+
size_t argc = 1;
|
|
562
|
+
napi_value argv[1];
|
|
563
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
564
|
+
if (argc < 1)
|
|
565
|
+
return ThrowCode(env, "PATH_UNSAFE", "openRoot requires a path");
|
|
566
|
+
|
|
567
|
+
napi_valuetype t;
|
|
568
|
+
NAPI_CHECK(napi_typeof(env, argv[0], &t));
|
|
569
|
+
if (t != napi_string)
|
|
570
|
+
return ThrowCode(env, "PATH_UNSAFE", "openRoot path must be a string");
|
|
571
|
+
|
|
572
|
+
std::string path;
|
|
573
|
+
if (!GetUtf8(env, argv[0], path))
|
|
574
|
+
return nullptr;
|
|
575
|
+
|
|
576
|
+
if (path.empty() || path[0] != '/') {
|
|
577
|
+
return ThrowCode(env, "PATH_UNSAFE", "openRoot requires absolute path");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
int cur_fd = open("/", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
581
|
+
if (cur_fd < 0) {
|
|
582
|
+
return ThrowErrno(env, "open", errno, "/");
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
size_t pos = 1;
|
|
586
|
+
while (pos < path.size()) {
|
|
587
|
+
size_t next = path.find('/', pos);
|
|
588
|
+
if (next == std::string::npos) next = path.size();
|
|
589
|
+
|
|
590
|
+
std::string seg = path.substr(pos, next - pos);
|
|
591
|
+
pos = next + 1;
|
|
592
|
+
|
|
593
|
+
if (seg.empty()) continue;
|
|
594
|
+
|
|
595
|
+
if (!IsSafeSegment(seg.c_str(), seg.size())) {
|
|
596
|
+
SafeClose(cur_fd);
|
|
597
|
+
return ThrowErrno(env, "openat", EINVAL, seg.c_str());
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
int child_fd = openat(cur_fd, seg.c_str(),
|
|
601
|
+
O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
602
|
+
int saved_errno = errno;
|
|
603
|
+
SafeClose(cur_fd);
|
|
604
|
+
|
|
605
|
+
if (child_fd < 0) {
|
|
606
|
+
return ThrowErrno(env, "openat", saved_errno, seg.c_str());
|
|
607
|
+
}
|
|
608
|
+
cur_fd = child_fd;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return WrapDirFd(env, cur_fd);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ---------------------------------------------------------------------------
|
|
615
|
+
// openDir(handle, name) -> { _handle }
|
|
616
|
+
// ---------------------------------------------------------------------------
|
|
617
|
+
|
|
618
|
+
static napi_value OpenDir(napi_env env, napi_callback_info info) {
|
|
619
|
+
size_t argc = 4;
|
|
620
|
+
napi_value argv[4];
|
|
621
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
622
|
+
|
|
623
|
+
int parent_fd = GetDirFd(env, argv[0]);
|
|
624
|
+
if (parent_fd < 0)
|
|
625
|
+
return ThrowCode(env, "PATH_UNSAFE", "handle is closed");
|
|
626
|
+
|
|
627
|
+
auto seg = GetSegment(env, argv, argc, 1);
|
|
628
|
+
if (!seg.ok)
|
|
629
|
+
return ThrowCode(env, "PATH_UNSAFE", "openDir: unsafe segment");
|
|
630
|
+
|
|
631
|
+
int child_fd = openat(parent_fd, seg.value.c_str(),
|
|
632
|
+
O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
633
|
+
if (child_fd < 0)
|
|
634
|
+
return ThrowErrno(env, "openat", errno, seg.value.c_str());
|
|
635
|
+
|
|
636
|
+
return WrapDirFd(env, child_fd);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
// readEntry(handle, name) -> { type, size, mode, dev, ino, nlink } | null
|
|
641
|
+
// ---------------------------------------------------------------------------
|
|
642
|
+
|
|
643
|
+
static napi_value ReadEntry(napi_env env, napi_callback_info info) {
|
|
644
|
+
size_t argc = 4;
|
|
645
|
+
napi_value argv[4];
|
|
646
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
647
|
+
|
|
648
|
+
int dir_fd = GetDirFd(env, argv[0]);
|
|
649
|
+
if (dir_fd < 0)
|
|
650
|
+
return ThrowCode(env, "PATH_UNSAFE", "handle is closed");
|
|
651
|
+
|
|
652
|
+
// Allow "." for directory self-query (e.g. readEntry(".") to inspect own metadata)
|
|
653
|
+
auto seg = GetSegment(env, argv, argc, 1, /*allow_dot=*/true);
|
|
654
|
+
if (!seg.ok)
|
|
655
|
+
return ThrowCode(env, "PATH_UNSAFE", "readEntry: unsafe segment");
|
|
656
|
+
|
|
657
|
+
struct stat st;
|
|
658
|
+
// For "." use fstat on the directory fd itself (not fstatat on ".")
|
|
659
|
+
if (seg.value == ".") {
|
|
660
|
+
if (fstat(dir_fd, &st) < 0) {
|
|
661
|
+
return ThrowErrno(env, "fstat", errno, ".");
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
if (fstatat(dir_fd, seg.value.c_str(), &st, AT_SYMLINK_NOFOLLOW) < 0) {
|
|
665
|
+
if (errno == ENOENT) {
|
|
666
|
+
napi_value null_val;
|
|
667
|
+
napi_get_null(env, &null_val);
|
|
668
|
+
return null_val;
|
|
669
|
+
}
|
|
670
|
+
return ThrowErrno(env, "fstatat", errno, seg.value.c_str());
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
napi_value result;
|
|
675
|
+
NAPI_CHECK(napi_create_object(env, &result));
|
|
676
|
+
|
|
677
|
+
const char* type = "unknown";
|
|
678
|
+
if (S_ISREG(st.st_mode)) type = "file";
|
|
679
|
+
else if (S_ISDIR(st.st_mode)) type = "directory";
|
|
680
|
+
else if (S_ISLNK(st.st_mode)) type = "symlink";
|
|
681
|
+
else if (S_ISFIFO(st.st_mode)) type = "fifo";
|
|
682
|
+
else if (S_ISBLK(st.st_mode)) type = "block-device";
|
|
683
|
+
else if (S_ISCHR(st.st_mode)) type = "char-device";
|
|
684
|
+
else if (S_ISSOCK(st.st_mode)) type = "socket";
|
|
685
|
+
|
|
686
|
+
napi_value type_val, size_val, mode_val, dev_val, ino_val, nlink_val;
|
|
687
|
+
NAPI_CHECK(napi_create_string_utf8(env, type, NAPI_AUTO_LENGTH, &type_val));
|
|
688
|
+
NAPI_CHECK(
|
|
689
|
+
napi_create_int64(env, static_cast<int64_t>(st.st_size), &size_val));
|
|
690
|
+
NAPI_CHECK(
|
|
691
|
+
napi_create_int32(env, static_cast<int32_t>(st.st_mode & 07777),
|
|
692
|
+
&mode_val));
|
|
693
|
+
NAPI_CHECK(
|
|
694
|
+
napi_create_int64(env, static_cast<int64_t>(st.st_dev), &dev_val));
|
|
695
|
+
NAPI_CHECK(
|
|
696
|
+
napi_create_int64(env, static_cast<int64_t>(st.st_ino), &ino_val));
|
|
697
|
+
NAPI_CHECK(
|
|
698
|
+
napi_create_int64(env, static_cast<int64_t>(st.st_nlink), &nlink_val));
|
|
699
|
+
|
|
700
|
+
NAPI_CHECK(napi_set_named_property(env, result, "type", type_val));
|
|
701
|
+
NAPI_CHECK(napi_set_named_property(env, result, "size", size_val));
|
|
702
|
+
NAPI_CHECK(napi_set_named_property(env, result, "mode", mode_val));
|
|
703
|
+
NAPI_CHECK(napi_set_named_property(env, result, "dev", dev_val));
|
|
704
|
+
NAPI_CHECK(napi_set_named_property(env, result, "ino", ino_val));
|
|
705
|
+
NAPI_CHECK(napi_set_named_property(env, result, "nlink", nlink_val));
|
|
706
|
+
return result;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// ---------------------------------------------------------------------------
|
|
710
|
+
// readFile(handle, name) -> { bytes: Buffer, size, mode, dev, ino, nlink }
|
|
711
|
+
//
|
|
712
|
+
// Opens with O_RDONLY|O_NONBLOCK|O_NOFOLLOW|O_CLOEXEC to prevent FIFO
|
|
713
|
+
// blocking. Validates via fstat on the opened fd (regular, nlink=1),
|
|
714
|
+
// reads all bytes, then performs post-read identity verification.
|
|
715
|
+
// ---------------------------------------------------------------------------
|
|
716
|
+
|
|
717
|
+
static napi_value ReadFile(napi_env env, napi_callback_info info) {
|
|
718
|
+
size_t argc = 4;
|
|
719
|
+
napi_value argv[4];
|
|
720
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
721
|
+
|
|
722
|
+
int dir_fd = GetDirFd(env, argv[0]);
|
|
723
|
+
if (dir_fd < 0)
|
|
724
|
+
return ThrowCode(env, "PATH_UNSAFE", "handle is closed");
|
|
725
|
+
|
|
726
|
+
auto seg = GetSegment(env, argv, argc, 1);
|
|
727
|
+
if (!seg.ok)
|
|
728
|
+
return ThrowCode(env, "PATH_UNSAFE", "readFile: unsafe segment");
|
|
729
|
+
|
|
730
|
+
// Open with O_NONBLOCK to prevent blocking on FIFOs (security: B1).
|
|
731
|
+
// O_NOFOLLOW rejects symlinks at open time.
|
|
732
|
+
int file_fd = openat(dir_fd, seg.value.c_str(),
|
|
733
|
+
O_RDONLY | O_NONBLOCK | O_NOFOLLOW | O_CLOEXEC);
|
|
734
|
+
if (file_fd < 0) {
|
|
735
|
+
if (errno == ENOENT) {
|
|
736
|
+
napi_value null_val;
|
|
737
|
+
napi_get_null(env, &null_val);
|
|
738
|
+
return null_val;
|
|
739
|
+
}
|
|
740
|
+
return ThrowErrno(env, "openat", errno, seg.value.c_str());
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Validate via fstat on the opened fd
|
|
744
|
+
struct stat st;
|
|
745
|
+
if (fstat(file_fd, &st) < 0) {
|
|
746
|
+
int saved = errno;
|
|
747
|
+
SafeClose(file_fd);
|
|
748
|
+
return ThrowErrno(env, "fstat", saved, seg.value.c_str());
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (!S_ISREG(st.st_mode)) {
|
|
752
|
+
SafeClose(file_fd);
|
|
753
|
+
return ThrowCode(env, "PATH_UNSAFE", "readFile: not a regular file");
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (st.st_nlink != 1) {
|
|
757
|
+
SafeClose(file_fd);
|
|
758
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
759
|
+
"readFile: unexpected hard link count");
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Read all bytes — always loop (even from size=0) to handle concurrent
|
|
763
|
+
// growth, partial reads, and EINTR. Documented capacity limit: 256 MiB.
|
|
764
|
+
constexpr size_t kMaxReadCapacity = 256ULL * 1024 * 1024; // 256 MiB
|
|
765
|
+
std::vector<uint8_t> buf;
|
|
766
|
+
{
|
|
767
|
+
size_t init_size = (st.st_size > 0)
|
|
768
|
+
? static_cast<size_t>(st.st_size)
|
|
769
|
+
: 65536;
|
|
770
|
+
if (init_size > kMaxReadCapacity) init_size = kMaxReadCapacity;
|
|
771
|
+
buf.resize(init_size);
|
|
772
|
+
size_t total = 0;
|
|
773
|
+
while (true) {
|
|
774
|
+
size_t want = buf.size() - total;
|
|
775
|
+
if (want == 0) {
|
|
776
|
+
// Buffer full — check capacity before growing
|
|
777
|
+
if (buf.size() >= kMaxReadCapacity) {
|
|
778
|
+
SafeClose(file_fd);
|
|
779
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
780
|
+
"readFile: capacity limit exceeded (256 MiB)");
|
|
781
|
+
}
|
|
782
|
+
size_t new_cap = buf.size() + 65536;
|
|
783
|
+
if (new_cap > kMaxReadCapacity) new_cap = kMaxReadCapacity;
|
|
784
|
+
buf.resize(new_cap);
|
|
785
|
+
want = buf.size() - total;
|
|
786
|
+
}
|
|
787
|
+
ssize_t n = ::read(file_fd, buf.data() + total, want);
|
|
788
|
+
if (n < 0) {
|
|
789
|
+
if (errno == EINTR) continue;
|
|
790
|
+
int saved = errno;
|
|
791
|
+
SafeClose(file_fd);
|
|
792
|
+
return ThrowErrno(env, "read", saved, seg.value.c_str());
|
|
793
|
+
}
|
|
794
|
+
if (n == 0) break;
|
|
795
|
+
total += static_cast<size_t>(n);
|
|
796
|
+
}
|
|
797
|
+
buf.resize(total);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Post-read identity verification (B3): check that the file hasn't been
|
|
801
|
+
// concurrently replaced or modified. Compare dev/ino/type/nlink/size/mtime/ctime.
|
|
802
|
+
struct stat post_st;
|
|
803
|
+
if (fstat(file_fd, &post_st) < 0) {
|
|
804
|
+
int saved = errno;
|
|
805
|
+
SafeClose(file_fd);
|
|
806
|
+
return ThrowErrno(env, "fstat", saved, seg.value.c_str());
|
|
807
|
+
}
|
|
808
|
+
if (post_st.st_dev != st.st_dev || post_st.st_ino != st.st_ino ||
|
|
809
|
+
!S_ISREG(post_st.st_mode) || post_st.st_nlink != st.st_nlink) {
|
|
810
|
+
SafeClose(file_fd);
|
|
811
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
812
|
+
"readFile: file identity changed during read");
|
|
813
|
+
}
|
|
814
|
+
if (static_cast<size_t>(post_st.st_size) != buf.size()) {
|
|
815
|
+
SafeClose(file_fd);
|
|
816
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
817
|
+
"readFile: file size changed during read");
|
|
818
|
+
}
|
|
819
|
+
// Compare mtime and ctime (with nanosecond precision on supported platforms)
|
|
820
|
+
if (ST_MTIM_NS(post_st) != ST_MTIM_NS(st) ||
|
|
821
|
+
ST_CTIM_NS(post_st) != ST_CTIM_NS(st)) {
|
|
822
|
+
SafeClose(file_fd);
|
|
823
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
824
|
+
"readFile: file timestamp changed during read");
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
SafeClose(file_fd);
|
|
828
|
+
|
|
829
|
+
// Build result
|
|
830
|
+
napi_value result;
|
|
831
|
+
NAPI_CHECK(napi_create_object(env, &result));
|
|
832
|
+
|
|
833
|
+
napi_value bytes_val;
|
|
834
|
+
void* copy_data = nullptr;
|
|
835
|
+
NAPI_CHECK(napi_create_buffer_copy(env, buf.size(), buf.data(), ©_data,
|
|
836
|
+
&bytes_val));
|
|
837
|
+
(void)copy_data;
|
|
838
|
+
|
|
839
|
+
napi_value size_val, mode_val, dev_val, ino_val, nlink_val, mtime_val, ctime_val;
|
|
840
|
+
NAPI_CHECK(
|
|
841
|
+
napi_create_int64(env, static_cast<int64_t>(post_st.st_size), &size_val));
|
|
842
|
+
NAPI_CHECK(
|
|
843
|
+
napi_create_int32(env, static_cast<int32_t>(post_st.st_mode & 07777),
|
|
844
|
+
&mode_val));
|
|
845
|
+
NAPI_CHECK(
|
|
846
|
+
napi_create_int64(env, static_cast<int64_t>(post_st.st_dev), &dev_val));
|
|
847
|
+
NAPI_CHECK(
|
|
848
|
+
napi_create_int64(env, static_cast<int64_t>(post_st.st_ino), &ino_val));
|
|
849
|
+
NAPI_CHECK(
|
|
850
|
+
napi_create_int64(env, static_cast<int64_t>(post_st.st_nlink),
|
|
851
|
+
&nlink_val));
|
|
852
|
+
NAPI_CHECK(
|
|
853
|
+
napi_create_double(env, static_cast<double>(ST_MTIM_NS(post_st)),
|
|
854
|
+
&mtime_val));
|
|
855
|
+
NAPI_CHECK(
|
|
856
|
+
napi_create_double(env, static_cast<double>(ST_CTIM_NS(post_st)),
|
|
857
|
+
&ctime_val));
|
|
858
|
+
|
|
859
|
+
NAPI_CHECK(napi_set_named_property(env, result, "bytes", bytes_val));
|
|
860
|
+
NAPI_CHECK(napi_set_named_property(env, result, "size", size_val));
|
|
861
|
+
NAPI_CHECK(napi_set_named_property(env, result, "mode", mode_val));
|
|
862
|
+
NAPI_CHECK(napi_set_named_property(env, result, "dev", dev_val));
|
|
863
|
+
NAPI_CHECK(napi_set_named_property(env, result, "ino", ino_val));
|
|
864
|
+
NAPI_CHECK(napi_set_named_property(env, result, "nlink", nlink_val));
|
|
865
|
+
NAPI_CHECK(napi_set_named_property(env, result, "mtimeNs", mtime_val));
|
|
866
|
+
NAPI_CHECK(napi_set_named_property(env, result, "ctimeNs", ctime_val));
|
|
867
|
+
|
|
868
|
+
return result;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ---------------------------------------------------------------------------
|
|
872
|
+
// Identity-bound temp cleanup.
|
|
873
|
+
//
|
|
874
|
+
// Single cleanup path shared by all CreateTemp N-API failure branches and
|
|
875
|
+
// the TempTokenFinalize.
|
|
876
|
+
//
|
|
877
|
+
// identity_unlink(tok) — two-phase identity check:
|
|
878
|
+
// 1) fstat(tok->tmp_fd) on the open fd
|
|
879
|
+
// 2) fstatat(tok->dir_fd, tok->name) on the directory entry
|
|
880
|
+
// 3) Both must match each other AND creation-time dev/ino, be S_ISREG,
|
|
881
|
+
// and have nlink==1 before unlinking.
|
|
882
|
+
// On success, marks tok->active = false.
|
|
883
|
+
//
|
|
884
|
+
// Before napi_create_external: the caller owns tok and must delete it.
|
|
885
|
+
// Use cleanup_temp(tok) — identity-bound unlink, close fds, delete.
|
|
886
|
+
// After napi_create_external: tok is owned by the finalizer; use
|
|
887
|
+
// identity_unlink(tok) — identity-bound unlink only, do NOT close fds
|
|
888
|
+
// or delete (the finalizer handles that).
|
|
889
|
+
// ---------------------------------------------------------------------------
|
|
890
|
+
|
|
891
|
+
/** Identity-bound unlink: two-phase check (fd + directory entry) against
|
|
892
|
+
* creation-time identity before unlinking. Returns true if the file was
|
|
893
|
+
* unlinked, false if identity didn't match or unlink failed. */
|
|
894
|
+
static bool identity_unlink(TempToken* tok) {
|
|
895
|
+
if (!tok || tok->dir_fd < 0 || tok->name[0] == '\0') return false;
|
|
896
|
+
// Phase 1: fstat the open fd
|
|
897
|
+
struct stat fd_st;
|
|
898
|
+
if (fstat(tok->tmp_fd, &fd_st) < 0) return false;
|
|
899
|
+
if (!S_ISREG(fd_st.st_mode) || fd_st.st_nlink != 1) return false;
|
|
900
|
+
if (fd_st.st_dev != tok->tmp_dev || fd_st.st_ino != tok->tmp_ino)
|
|
901
|
+
return false;
|
|
902
|
+
// Phase 2: fstatat the directory entry
|
|
903
|
+
struct stat dir_st;
|
|
904
|
+
if (fstatat(tok->dir_fd, tok->name, &dir_st, AT_SYMLINK_NOFOLLOW) < 0)
|
|
905
|
+
return false;
|
|
906
|
+
if (!S_ISREG(dir_st.st_mode) || dir_st.st_nlink != 1) return false;
|
|
907
|
+
if (dir_st.st_dev != tok->tmp_dev || dir_st.st_ino != tok->tmp_ino)
|
|
908
|
+
return false;
|
|
909
|
+
// Both phases passed — unlink
|
|
910
|
+
if (unlinkat(tok->dir_fd, tok->name, 0) == 0) {
|
|
911
|
+
tok->active = false;
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** Identity-bound unlink, close+delete the token (caller owns tok). */
|
|
918
|
+
static void cleanup_temp(TempToken* tok) {
|
|
919
|
+
if (!tok) return;
|
|
920
|
+
identity_unlink(tok);
|
|
921
|
+
tok->CloseFds();
|
|
922
|
+
delete tok;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ---------------------------------------------------------------------------
|
|
926
|
+
// createTemp(handle, name, mode, Buffer) -> { tempName, _token }
|
|
927
|
+
//
|
|
928
|
+
// Validates: name is safe segment, mode is 0..0777, data is Buffer.
|
|
929
|
+
// Creates temp file, writes data, fsyncs, applies fchmod for exact mode,
|
|
930
|
+
// captures identity, and returns a type-tagged TempToken.
|
|
931
|
+
// ---------------------------------------------------------------------------
|
|
932
|
+
|
|
933
|
+
static napi_value CreateTemp(napi_env env, napi_callback_info info) {
|
|
934
|
+
size_t argc = 4;
|
|
935
|
+
napi_value argv[4];
|
|
936
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
937
|
+
|
|
938
|
+
int dir_fd = GetDirFd(env, argv[0]);
|
|
939
|
+
if (dir_fd < 0)
|
|
940
|
+
return ThrowCode(env, "PATH_UNSAFE", "handle is closed");
|
|
941
|
+
|
|
942
|
+
if (argc < 4)
|
|
943
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
944
|
+
"createTemp requires name, mode, and data");
|
|
945
|
+
|
|
946
|
+
auto seg = GetSegment(env, argv, argc, 1);
|
|
947
|
+
if (!seg.ok)
|
|
948
|
+
return ThrowCode(env, "PATH_UNSAFE", "createTemp: unsafe name segment");
|
|
949
|
+
|
|
950
|
+
// Validate mode type (must be int32) before extracting value (A2)
|
|
951
|
+
{
|
|
952
|
+
napi_valuetype mt;
|
|
953
|
+
NAPI_CHECK(napi_typeof(env, argv[2], &mt));
|
|
954
|
+
if (mt != napi_number) {
|
|
955
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
956
|
+
"createTemp: mode must be a number");
|
|
957
|
+
}
|
|
958
|
+
// Reject NaN and non-integer doubles (e.g. 644.5, Infinity)
|
|
959
|
+
double dval = 0;
|
|
960
|
+
NAPI_CHECK(napi_get_value_double(env, argv[2], &dval));
|
|
961
|
+
if (dval != dval || dval == INFINITY) { // NaN check via self-inequality
|
|
962
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
963
|
+
"createTemp: mode must be an integer");
|
|
964
|
+
}
|
|
965
|
+
if (static_cast<double>(static_cast<int32_t>(dval)) != dval) {
|
|
966
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
967
|
+
"createTemp: mode must be an integer");
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Validate mode range (A2)
|
|
972
|
+
int32_t mode = 0;
|
|
973
|
+
if (!GetInt32(env, argv[2], mode))
|
|
974
|
+
return nullptr;
|
|
975
|
+
if (mode < 0 || mode > 0777) {
|
|
976
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
977
|
+
"createTemp: mode must be 0..0777");
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Require Buffer data — empty Buffer is legal, but must be a Buffer (A2)
|
|
981
|
+
if (!argv[3]) {
|
|
982
|
+
return ThrowCode(env, "PATH_UNSAFE", "createTemp: data must be a Buffer");
|
|
983
|
+
}
|
|
984
|
+
{
|
|
985
|
+
napi_valuetype dt;
|
|
986
|
+
NAPI_CHECK(napi_typeof(env, argv[3], &dt));
|
|
987
|
+
if (dt != napi_object) {
|
|
988
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
989
|
+
"createTemp: data must be a Buffer");
|
|
990
|
+
}
|
|
991
|
+
bool is_buf = false;
|
|
992
|
+
NAPI_CHECK(napi_is_buffer(env, argv[3], &is_buf));
|
|
993
|
+
if (!is_buf) {
|
|
994
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
995
|
+
"createTemp: data must be a Buffer");
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const uint8_t* data = nullptr;
|
|
1000
|
+
size_t data_len = 0;
|
|
1001
|
+
NAPI_CHECK(napi_get_buffer_info(env, argv[3], (void**)&data, &data_len));
|
|
1002
|
+
|
|
1003
|
+
// Generate temp name
|
|
1004
|
+
uint64_t r = 0;
|
|
1005
|
+
#if defined(__APPLE__) || defined(__FreeBSD__)
|
|
1006
|
+
r = (static_cast<uint64_t>(arc4random()) << 32) | arc4random();
|
|
1007
|
+
#else
|
|
1008
|
+
r = (static_cast<uint64_t>(random()) << 32) | random();
|
|
1009
|
+
#endif
|
|
1010
|
+
char tmp_name[PATH_MAX];
|
|
1011
|
+
int name_len = snprintf(tmp_name, sizeof(tmp_name), ".tmp-%s-%d-%llx",
|
|
1012
|
+
seg.value.c_str(), static_cast<int>(getpid()),
|
|
1013
|
+
static_cast<unsigned long long>(r));
|
|
1014
|
+
if (name_len < 0 ||
|
|
1015
|
+
static_cast<size_t>(name_len) >= sizeof(tmp_name)) {
|
|
1016
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1017
|
+
"createTemp: temp name truncated");
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
int tmp_fd = openat(dir_fd, tmp_name,
|
|
1021
|
+
O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC,
|
|
1022
|
+
mode);
|
|
1023
|
+
if (tmp_fd < 0) return ThrowErrno(env, "openat", errno, tmp_name);
|
|
1024
|
+
|
|
1025
|
+
// Write data in a loop (handles partial writes and EINTR)
|
|
1026
|
+
if (data_len > 0) {
|
|
1027
|
+
size_t written = 0;
|
|
1028
|
+
while (written < data_len) {
|
|
1029
|
+
ssize_t n = ::write(tmp_fd, data + written, data_len - written);
|
|
1030
|
+
if (n < 0) {
|
|
1031
|
+
if (errno == EINTR) continue;
|
|
1032
|
+
int saved = errno;
|
|
1033
|
+
SafeClose(tmp_fd);
|
|
1034
|
+
unlinkat(dir_fd, tmp_name, 0);
|
|
1035
|
+
return ThrowErrno(env, "write", saved, tmp_name);
|
|
1036
|
+
}
|
|
1037
|
+
if (n == 0) {
|
|
1038
|
+
SafeClose(tmp_fd);
|
|
1039
|
+
unlinkat(dir_fd, tmp_name, 0);
|
|
1040
|
+
return ThrowErrno(env, "write", EIO, tmp_name);
|
|
1041
|
+
}
|
|
1042
|
+
written += static_cast<size_t>(n);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Apply exact mode via fchmod on the fd BEFORE fsync (A2).
|
|
1047
|
+
// This ensures the mode is persisted with the data.
|
|
1048
|
+
if (fchmod(tmp_fd, mode) < 0) {
|
|
1049
|
+
int saved = errno;
|
|
1050
|
+
SafeClose(tmp_fd);
|
|
1051
|
+
unlinkat(dir_fd, tmp_name, 0);
|
|
1052
|
+
return ThrowErrno(env, "fchmod", saved, tmp_name);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// fsync data AND mode to disk before identity capture
|
|
1056
|
+
if (fsync(tmp_fd) < 0) {
|
|
1057
|
+
int saved = errno;
|
|
1058
|
+
SafeClose(tmp_fd);
|
|
1059
|
+
unlinkat(dir_fd, tmp_name, 0);
|
|
1060
|
+
return ThrowErrno(env, "fsync", saved, tmp_name);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Capture identity of the temp file via fstat on the open fd
|
|
1064
|
+
struct stat tmp_st;
|
|
1065
|
+
if (fstat(tmp_fd, &tmp_st) < 0) {
|
|
1066
|
+
int saved = errno;
|
|
1067
|
+
SafeClose(tmp_fd);
|
|
1068
|
+
unlinkat(dir_fd, tmp_name, 0);
|
|
1069
|
+
return ThrowErrno(env, "fstat", saved, tmp_name);
|
|
1070
|
+
}
|
|
1071
|
+
if (!S_ISREG(tmp_st.st_mode) || tmp_st.st_nlink != 1) {
|
|
1072
|
+
SafeClose(tmp_fd);
|
|
1073
|
+
unlinkat(dir_fd, tmp_name, 0);
|
|
1074
|
+
return ThrowCode(env, "PATH_UNSAFE", "createTemp: identity check failed");
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// dup the directory fd with CLOEXEC (A5)
|
|
1078
|
+
int token_dir_fd = fcntl(dir_fd, F_DUPFD_CLOEXEC, 0);
|
|
1079
|
+
if (token_dir_fd < 0) {
|
|
1080
|
+
int saved = errno;
|
|
1081
|
+
SafeClose(tmp_fd);
|
|
1082
|
+
unlinkat(dir_fd, tmp_name, 0);
|
|
1083
|
+
return ThrowErrno(env, "fcntl(F_DUPFD_CLOEXEC)", saved, "<dir>");
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Build the TempToken
|
|
1087
|
+
TempToken* tok = new (std::nothrow) TempToken();
|
|
1088
|
+
if (!tok) {
|
|
1089
|
+
SafeClose(tmp_fd);
|
|
1090
|
+
SafeClose(token_dir_fd);
|
|
1091
|
+
unlinkat(dir_fd, tmp_name, 0);
|
|
1092
|
+
return ThrowCode(env, "PATH_UNSAFE", "createTemp: allocation failed");
|
|
1093
|
+
}
|
|
1094
|
+
tok->tmp_fd = tmp_fd;
|
|
1095
|
+
tok->dir_fd = token_dir_fd;
|
|
1096
|
+
tok->tmp_dev = tmp_st.st_dev;
|
|
1097
|
+
tok->tmp_ino = tmp_st.st_ino;
|
|
1098
|
+
tok->active = true;
|
|
1099
|
+
tok->committed = false;
|
|
1100
|
+
strncpy(tok->name, tmp_name, PATH_MAX - 1);
|
|
1101
|
+
tok->name[PATH_MAX - 1] = '\0';
|
|
1102
|
+
|
|
1103
|
+
// Build return object: { tempName, _token }
|
|
1104
|
+
// On any N-API failure, unlink the temp file before closing fds to prevent
|
|
1105
|
+
// orphan temp files. cleanup_temp(tok) performs identity-bound unlink
|
|
1106
|
+
// then closes fds and deletes the token.
|
|
1107
|
+
//
|
|
1108
|
+
// Test seam points fire BEFORE the real N-API call. When a seam fires,
|
|
1109
|
+
// it throws PATH_UNSAFE (stable error code) and returns immediately,
|
|
1110
|
+
// skipping the real N-API call. This ensures deterministic testing:
|
|
1111
|
+
// the file stays on disk (no real unlinkat) and the error has a stable code.
|
|
1112
|
+
napi_value result;
|
|
1113
|
+
#ifdef SAFE_WRITE_TEST_SEAM
|
|
1114
|
+
if (TestSeamShouldFail(kSeamCreateObject)) {
|
|
1115
|
+
cleanup_temp(tok);
|
|
1116
|
+
return ThrowCode(env, "PATH_UNSAFE", "test seam: createTemp create_object");
|
|
1117
|
+
}
|
|
1118
|
+
#endif
|
|
1119
|
+
if (napi_create_object(env, &result) != napi_ok) {
|
|
1120
|
+
cleanup_temp(tok);
|
|
1121
|
+
napi_throw_error(env, nullptr, "N-API call failed: napi_create_object");
|
|
1122
|
+
return nullptr;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
napi_value name_val;
|
|
1126
|
+
#ifdef SAFE_WRITE_TEST_SEAM
|
|
1127
|
+
if (TestSeamShouldFail(kSeamCreateString)) {
|
|
1128
|
+
cleanup_temp(tok);
|
|
1129
|
+
return ThrowCode(env, "PATH_UNSAFE", "test seam: createTemp create_string");
|
|
1130
|
+
}
|
|
1131
|
+
#endif
|
|
1132
|
+
if (napi_create_string_utf8(env, tmp_name, NAPI_AUTO_LENGTH, &name_val) !=
|
|
1133
|
+
napi_ok) {
|
|
1134
|
+
cleanup_temp(tok);
|
|
1135
|
+
napi_throw_error(env, nullptr,
|
|
1136
|
+
"N-API call failed: napi_create_string_utf8");
|
|
1137
|
+
return nullptr;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
napi_value ext;
|
|
1141
|
+
#ifdef SAFE_WRITE_TEST_SEAM
|
|
1142
|
+
if (TestSeamShouldFail(kSeamCreateExternal)) {
|
|
1143
|
+
cleanup_temp(tok);
|
|
1144
|
+
return ThrowCode(env, "PATH_UNSAFE", "test seam: createTemp create_external");
|
|
1145
|
+
}
|
|
1146
|
+
#endif
|
|
1147
|
+
if (napi_create_external(env, tok, TempTokenFinalize, nullptr, &ext) !=
|
|
1148
|
+
napi_ok) {
|
|
1149
|
+
cleanup_temp(tok);
|
|
1150
|
+
napi_throw_error(env, nullptr, "N-API call failed: napi_create_external");
|
|
1151
|
+
return nullptr;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// After napi_create_external succeeded, tok is owned by the finalizer.
|
|
1155
|
+
// We must NOT delete tok or close fds — use identity_unlink(tok) for
|
|
1156
|
+
// identity-bound unlink + mark inactive.
|
|
1157
|
+
#ifdef SAFE_WRITE_TEST_SEAM
|
|
1158
|
+
if (TestSeamShouldFail(kSeamSetProperty)) {
|
|
1159
|
+
identity_unlink(tok);
|
|
1160
|
+
return ThrowCode(env, "PATH_UNSAFE", "test seam: createTemp set_property");
|
|
1161
|
+
}
|
|
1162
|
+
#endif
|
|
1163
|
+
if (napi_set_named_property(env, result, "tempName", name_val) != napi_ok ||
|
|
1164
|
+
napi_set_named_property(env, result, "_token", ext) != napi_ok) {
|
|
1165
|
+
identity_unlink(tok);
|
|
1166
|
+
napi_throw_error(env, nullptr,
|
|
1167
|
+
"N-API call failed: napi_set_named_property");
|
|
1168
|
+
return nullptr;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// Tag the token object (A3)
|
|
1172
|
+
napi_type_tag tok_tag = {kTempTokenTag, 0};
|
|
1173
|
+
#ifdef SAFE_WRITE_TEST_SEAM
|
|
1174
|
+
if (TestSeamShouldFail(kSeamTypeTag)) {
|
|
1175
|
+
identity_unlink(tok);
|
|
1176
|
+
return ThrowCode(env, "PATH_UNSAFE", "test seam: createTemp type_tag");
|
|
1177
|
+
}
|
|
1178
|
+
#endif
|
|
1179
|
+
if (napi_type_tag_object(env, result, &tok_tag) != napi_ok) {
|
|
1180
|
+
identity_unlink(tok);
|
|
1181
|
+
napi_throw_error(env, nullptr, "N-API call failed: napi_type_tag_object");
|
|
1182
|
+
return nullptr;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
return result;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// ---------------------------------------------------------------------------
|
|
1189
|
+
// rename(handle, token, targetSegment)
|
|
1190
|
+
// ---------------------------------------------------------------------------
|
|
1191
|
+
|
|
1192
|
+
static napi_value Rename(napi_env env, napi_callback_info info) {
|
|
1193
|
+
size_t argc = 4;
|
|
1194
|
+
napi_value argv[4];
|
|
1195
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
1196
|
+
|
|
1197
|
+
int dir_fd = GetDirFd(env, argv[0]);
|
|
1198
|
+
if (dir_fd < 0)
|
|
1199
|
+
return ThrowCode(env, "PATH_UNSAFE", "handle is closed");
|
|
1200
|
+
|
|
1201
|
+
if (argc < 3)
|
|
1202
|
+
return ThrowCode(env, "PATH_UNSAFE", "rename requires token and target");
|
|
1203
|
+
|
|
1204
|
+
// Unwrap token with type tag verification (A3)
|
|
1205
|
+
TempToken* tok = GetTempToken(env, argv[1]);
|
|
1206
|
+
if (!tok)
|
|
1207
|
+
return ThrowCode(env, "PATH_UNSAFE", "rename: invalid token");
|
|
1208
|
+
|
|
1209
|
+
if (!tok->active)
|
|
1210
|
+
return ThrowCode(env, "PATH_UNSAFE", "rename: token already consumed");
|
|
1211
|
+
if (tok->committed)
|
|
1212
|
+
return ThrowCode(env, "PATH_UNSAFE", "rename: token already committed");
|
|
1213
|
+
|
|
1214
|
+
auto to_seg = GetSegment(env, argv, argc, 2);
|
|
1215
|
+
if (!to_seg.ok)
|
|
1216
|
+
return ThrowCode(env, "PATH_UNSAFE", "rename: unsafe target segment");
|
|
1217
|
+
|
|
1218
|
+
// 1. Verify directory identity
|
|
1219
|
+
if (tok->dir_fd < 0)
|
|
1220
|
+
return ThrowCode(env, "PATH_UNSAFE", "rename: token directory fd invalid");
|
|
1221
|
+
|
|
1222
|
+
struct stat caller_dir_st, token_dir_st;
|
|
1223
|
+
if (fstat(dir_fd, &caller_dir_st) < 0)
|
|
1224
|
+
return ThrowErrno(env, "fstat", errno, "<dir>");
|
|
1225
|
+
if (fstat(tok->dir_fd, &token_dir_st) < 0)
|
|
1226
|
+
return ThrowErrno(env, "fstat", errno, "<token-dir>");
|
|
1227
|
+
|
|
1228
|
+
if (caller_dir_st.st_dev != token_dir_st.st_dev ||
|
|
1229
|
+
caller_dir_st.st_ino != token_dir_st.st_ino) {
|
|
1230
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1231
|
+
"rename: token directory identity mismatch");
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// 2. Verify temp file identity — TWO-PHASE THREE-PARTY CHECK (RED 8):
|
|
1235
|
+
// a) fstat(tok->tmp_fd) — the open fd (attack-resistant)
|
|
1236
|
+
// b) fstatat(tok->dir_fd, tok->name) — the directory entry by name
|
|
1237
|
+
// c) Both must match each other AND the creation-time identity
|
|
1238
|
+
struct stat tmp_fd_st;
|
|
1239
|
+
if (fstat(tok->tmp_fd, &tmp_fd_st) < 0) {
|
|
1240
|
+
return ThrowErrno(env, "fstat", errno, "<tmp_fd>");
|
|
1241
|
+
}
|
|
1242
|
+
struct stat tmp_st;
|
|
1243
|
+
if (fstatat(tok->dir_fd, tok->name, &tmp_st, AT_SYMLINK_NOFOLLOW) < 0) {
|
|
1244
|
+
return ThrowErrno(env, "fstatat", errno, tok->name);
|
|
1245
|
+
}
|
|
1246
|
+
// fd and name must point to the same file
|
|
1247
|
+
if (tmp_fd_st.st_dev != tmp_st.st_dev ||
|
|
1248
|
+
tmp_fd_st.st_ino != tmp_st.st_ino) {
|
|
1249
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1250
|
+
"rename: temp fd/name identity mismatch");
|
|
1251
|
+
}
|
|
1252
|
+
if (!S_ISREG(tmp_fd_st.st_mode))
|
|
1253
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1254
|
+
"rename: temp is no longer a regular file");
|
|
1255
|
+
if (tmp_fd_st.st_nlink != 1)
|
|
1256
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1257
|
+
"rename: temp has unexpected hard link count");
|
|
1258
|
+
if (tmp_fd_st.st_dev != tok->tmp_dev ||
|
|
1259
|
+
tmp_fd_st.st_ino != tok->tmp_ino) {
|
|
1260
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1261
|
+
"rename: temp identity mismatch (dev/ino changed)");
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// 3. Commit. With an expected identity object, atomically exchange the
|
|
1265
|
+
// temp and existing target, prove that the displaced entry is the exact
|
|
1266
|
+
// entry read by the caller, then unlink it. Without expected identity the
|
|
1267
|
+
// operation remains strict no-replace creation.
|
|
1268
|
+
bool replacing = argc >= 4;
|
|
1269
|
+
int old_fd = -1;
|
|
1270
|
+
if (replacing) {
|
|
1271
|
+
ExpectedIdentity expected;
|
|
1272
|
+
if (!GetExpectedIdentity(env, argv[3], expected)) {
|
|
1273
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1274
|
+
"rename: invalid expected identity token");
|
|
1275
|
+
}
|
|
1276
|
+
old_fd = openat(dir_fd, to_seg.value.c_str(),
|
|
1277
|
+
O_RDONLY | O_NONBLOCK | O_NOFOLLOW | O_CLOEXEC);
|
|
1278
|
+
if (old_fd < 0)
|
|
1279
|
+
return ThrowErrno(env, "openat", errno, to_seg.value.c_str());
|
|
1280
|
+
|
|
1281
|
+
struct stat old_fd_st, old_name_st;
|
|
1282
|
+
if (fstat(old_fd, &old_fd_st) < 0 ||
|
|
1283
|
+
fstatat(dir_fd, to_seg.value.c_str(), &old_name_st,
|
|
1284
|
+
AT_SYMLINK_NOFOLLOW) < 0) {
|
|
1285
|
+
int saved = errno;
|
|
1286
|
+
SafeClose(old_fd);
|
|
1287
|
+
return ThrowErrno(env, "fstat", saved, to_seg.value.c_str());
|
|
1288
|
+
}
|
|
1289
|
+
if (!StatMatchesExpected(old_fd_st, expected) ||
|
|
1290
|
+
old_fd_st.st_dev != old_name_st.st_dev ||
|
|
1291
|
+
old_fd_st.st_ino != old_name_st.st_ino ||
|
|
1292
|
+
!StatMatchesExpected(old_name_st, expected)) {
|
|
1293
|
+
SafeClose(old_fd);
|
|
1294
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1295
|
+
"rename: expected target identity changed");
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
if (safe_rename_exchange(tok->dir_fd, tok->name, dir_fd,
|
|
1299
|
+
to_seg.value.c_str()) < 0) {
|
|
1300
|
+
int saved = errno;
|
|
1301
|
+
SafeClose(old_fd);
|
|
1302
|
+
if (saved == ENOSYS) {
|
|
1303
|
+
return ThrowCode(env, "SAFE_WRITE_UNAVAILABLE",
|
|
1304
|
+
"atomic exchange rename not supported on platform");
|
|
1305
|
+
}
|
|
1306
|
+
return ThrowErrno(env, "renameat_exchange", saved,
|
|
1307
|
+
to_seg.value.c_str());
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
struct stat displaced_st, old_post_st, new_target_st;
|
|
1311
|
+
bool exchange_valid =
|
|
1312
|
+
fstatat(tok->dir_fd, tok->name, &displaced_st,
|
|
1313
|
+
AT_SYMLINK_NOFOLLOW) == 0 &&
|
|
1314
|
+
fstat(old_fd, &old_post_st) == 0 &&
|
|
1315
|
+
fstatat(dir_fd, to_seg.value.c_str(), &new_target_st,
|
|
1316
|
+
AT_SYMLINK_NOFOLLOW) == 0 &&
|
|
1317
|
+
displaced_st.st_dev == old_post_st.st_dev &&
|
|
1318
|
+
displaced_st.st_ino == old_post_st.st_ino &&
|
|
1319
|
+
StatMatchesExpectedAfterRename(old_post_st, expected) &&
|
|
1320
|
+
StatMatchesExpectedAfterRename(displaced_st, expected) &&
|
|
1321
|
+
new_target_st.st_dev == tok->tmp_dev &&
|
|
1322
|
+
new_target_st.st_ino == tok->tmp_ino &&
|
|
1323
|
+
S_ISREG(new_target_st.st_mode) && new_target_st.st_nlink == 1;
|
|
1324
|
+
|
|
1325
|
+
if (!exchange_valid) {
|
|
1326
|
+
int rollback_rc = safe_rename_exchange(tok->dir_fd, tok->name, dir_fd,
|
|
1327
|
+
to_seg.value.c_str());
|
|
1328
|
+
SafeClose(old_fd);
|
|
1329
|
+
if (rollback_rc < 0) {
|
|
1330
|
+
tok->active = false;
|
|
1331
|
+
tok->committed = true;
|
|
1332
|
+
SafeClose(tok->tmp_fd);
|
|
1333
|
+
tok->tmp_fd = -1;
|
|
1334
|
+
SafeClose(tok->dir_fd);
|
|
1335
|
+
tok->dir_fd = -1;
|
|
1336
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1337
|
+
"rename: exchange verification and rollback failed");
|
|
1338
|
+
}
|
|
1339
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1340
|
+
"rename: expected target changed during exchange");
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
if (unlinkat(tok->dir_fd, tok->name, 0) < 0) {
|
|
1344
|
+
int saved = errno;
|
|
1345
|
+
SafeClose(old_fd);
|
|
1346
|
+
tok->active = false;
|
|
1347
|
+
tok->committed = true;
|
|
1348
|
+
SafeClose(tok->tmp_fd);
|
|
1349
|
+
tok->tmp_fd = -1;
|
|
1350
|
+
SafeClose(tok->dir_fd);
|
|
1351
|
+
tok->dir_fd = -1;
|
|
1352
|
+
return ThrowErrno(env, "unlinkat", saved, tok->name);
|
|
1353
|
+
}
|
|
1354
|
+
SafeClose(old_fd);
|
|
1355
|
+
} else {
|
|
1356
|
+
int rc = safe_rename_noreplace(tok->dir_fd, tok->name, dir_fd,
|
|
1357
|
+
to_seg.value.c_str());
|
|
1358
|
+
if (rc < 0) {
|
|
1359
|
+
int saved = errno;
|
|
1360
|
+
if (saved == EEXIST || saved == ENOTEMPTY) {
|
|
1361
|
+
return ThrowCode(env, "PATH_UNSAFE", "rename target already exists");
|
|
1362
|
+
}
|
|
1363
|
+
if (saved == ENOSYS) {
|
|
1364
|
+
return ThrowCode(env, "SAFE_WRITE_UNAVAILABLE",
|
|
1365
|
+
"atomic no-replace rename not supported on platform");
|
|
1366
|
+
}
|
|
1367
|
+
return ThrowErrno(env, "renameat_noreplace", saved,
|
|
1368
|
+
to_seg.value.c_str());
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// 4. Verify target identity matches token (B3).
|
|
1373
|
+
// After rename, target must match the still-open token fd (attack-resistant).
|
|
1374
|
+
// Mark committed immediately after rename succeeds (RED 8: rename once
|
|
1375
|
+
// successful is immediately marked committed/consumed).
|
|
1376
|
+
tok->active = false;
|
|
1377
|
+
tok->committed = true;
|
|
1378
|
+
|
|
1379
|
+
struct stat target_st;
|
|
1380
|
+
if (fstatat(dir_fd, to_seg.value.c_str(), &target_st,
|
|
1381
|
+
AT_SYMLINK_NOFOLLOW) < 0) {
|
|
1382
|
+
// Rename succeeded but can't verify target by name — uncertain durability.
|
|
1383
|
+
// Token already marked committed; close fds.
|
|
1384
|
+
int saved = errno;
|
|
1385
|
+
SafeClose(tok->tmp_fd);
|
|
1386
|
+
tok->tmp_fd = -1;
|
|
1387
|
+
SafeClose(tok->dir_fd);
|
|
1388
|
+
tok->dir_fd = -1;
|
|
1389
|
+
return ThrowErrno(env, "fstatat", saved, to_seg.value.c_str());
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Cross-check with the still-open token fd
|
|
1393
|
+
struct stat post_fd_st;
|
|
1394
|
+
if (fstat(tok->tmp_fd, &post_fd_st) < 0) {
|
|
1395
|
+
int saved = errno;
|
|
1396
|
+
SafeClose(tok->tmp_fd);
|
|
1397
|
+
tok->tmp_fd = -1;
|
|
1398
|
+
SafeClose(tok->dir_fd);
|
|
1399
|
+
tok->dir_fd = -1;
|
|
1400
|
+
return ThrowErrno(env, "fstat", saved, "<tmp_fd>");
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Verify target, token fd, and creation identity all match
|
|
1404
|
+
if (target_st.st_dev != tok->tmp_dev ||
|
|
1405
|
+
target_st.st_ino != tok->tmp_ino) {
|
|
1406
|
+
SafeClose(tok->tmp_fd);
|
|
1407
|
+
tok->tmp_fd = -1;
|
|
1408
|
+
SafeClose(tok->dir_fd);
|
|
1409
|
+
tok->dir_fd = -1;
|
|
1410
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1411
|
+
"rename: target identity mismatch");
|
|
1412
|
+
}
|
|
1413
|
+
if (post_fd_st.st_dev != tok->tmp_dev ||
|
|
1414
|
+
post_fd_st.st_ino != tok->tmp_ino) {
|
|
1415
|
+
SafeClose(tok->tmp_fd);
|
|
1416
|
+
tok->tmp_fd = -1;
|
|
1417
|
+
SafeClose(tok->dir_fd);
|
|
1418
|
+
tok->dir_fd = -1;
|
|
1419
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1420
|
+
"rename: token fd identity changed after rename");
|
|
1421
|
+
}
|
|
1422
|
+
if (!S_ISREG(target_st.st_mode) || target_st.st_nlink != 1) {
|
|
1423
|
+
SafeClose(tok->tmp_fd);
|
|
1424
|
+
tok->tmp_fd = -1;
|
|
1425
|
+
SafeClose(tok->dir_fd);
|
|
1426
|
+
tok->dir_fd = -1;
|
|
1427
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1428
|
+
"rename: target is not regular or nlink!=1");
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// 5. Fsync directory
|
|
1432
|
+
int fsync_rc;
|
|
1433
|
+
do {
|
|
1434
|
+
fsync_rc = fsync(dir_fd);
|
|
1435
|
+
} while (fsync_rc < 0 && errno == EINTR);
|
|
1436
|
+
if (fsync_rc < 0) {
|
|
1437
|
+
int saved = errno;
|
|
1438
|
+
// Target exists and identity verified, but directory not synced.
|
|
1439
|
+
// Mark committed since rename is durable on most filesystems.
|
|
1440
|
+
tok->active = false;
|
|
1441
|
+
tok->committed = true;
|
|
1442
|
+
SafeClose(tok->tmp_fd);
|
|
1443
|
+
tok->tmp_fd = -1;
|
|
1444
|
+
SafeClose(tok->dir_fd);
|
|
1445
|
+
tok->dir_fd = -1;
|
|
1446
|
+
return ThrowErrno(env, "fsync", saved, "<dir>");
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// 6. Consume token
|
|
1450
|
+
tok->active = false;
|
|
1451
|
+
tok->committed = true;
|
|
1452
|
+
SafeClose(tok->tmp_fd);
|
|
1453
|
+
tok->tmp_fd = -1;
|
|
1454
|
+
SafeClose(tok->dir_fd);
|
|
1455
|
+
tok->dir_fd = -1;
|
|
1456
|
+
|
|
1457
|
+
napi_value undefined;
|
|
1458
|
+
NAPI_CHECK(napi_get_undefined(env, &undefined));
|
|
1459
|
+
return undefined;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// ---------------------------------------------------------------------------
|
|
1463
|
+
// abortTemp(handle, token)
|
|
1464
|
+
// ---------------------------------------------------------------------------
|
|
1465
|
+
|
|
1466
|
+
static napi_value AbortTemp(napi_env env, napi_callback_info info) {
|
|
1467
|
+
size_t argc = 4;
|
|
1468
|
+
napi_value argv[4];
|
|
1469
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
1470
|
+
|
|
1471
|
+
int dir_fd = GetDirFd(env, argv[0]);
|
|
1472
|
+
if (dir_fd < 0)
|
|
1473
|
+
return ThrowCode(env, "PATH_UNSAFE", "handle is closed");
|
|
1474
|
+
|
|
1475
|
+
if (argc < 2)
|
|
1476
|
+
return ThrowCode(env, "PATH_UNSAFE", "abortTemp requires token");
|
|
1477
|
+
|
|
1478
|
+
// Unwrap token with type tag verification (A3)
|
|
1479
|
+
TempToken* tok = GetTempToken(env, argv[1]);
|
|
1480
|
+
if (!tok)
|
|
1481
|
+
return ThrowCode(env, "PATH_UNSAFE", "abortTemp: invalid token");
|
|
1482
|
+
|
|
1483
|
+
if (!tok->active) {
|
|
1484
|
+
return ThrowCode(env, "PATH_UNSAFE", "abortTemp: token already consumed");
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Verify caller directory matches token directory (cross-directory abort)
|
|
1488
|
+
if (tok->dir_fd >= 0) {
|
|
1489
|
+
struct stat caller_dir_st, token_dir_st;
|
|
1490
|
+
if (fstat(dir_fd, &caller_dir_st) < 0)
|
|
1491
|
+
return ThrowErrno(env, "fstat", errno, "<dir>");
|
|
1492
|
+
if (fstat(tok->dir_fd, &token_dir_st) < 0)
|
|
1493
|
+
return ThrowErrno(env, "fstat", errno, "<token-dir>");
|
|
1494
|
+
if (caller_dir_st.st_dev != token_dir_st.st_dev ||
|
|
1495
|
+
caller_dir_st.st_ino != token_dir_st.st_ino) {
|
|
1496
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1497
|
+
"abortTemp: directory identity mismatch");
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// Determine result before consuming token.
|
|
1502
|
+
// Returns {removed: boolean, reason: string} per frozen contract.
|
|
1503
|
+
bool removed = false;
|
|
1504
|
+
const char* reason = "unknown";
|
|
1505
|
+
|
|
1506
|
+
if (tok->committed) {
|
|
1507
|
+
removed = false;
|
|
1508
|
+
reason = "already_committed";
|
|
1509
|
+
} else if (tok->dir_fd < 0 || tok->name[0] == '\0') {
|
|
1510
|
+
removed = false;
|
|
1511
|
+
reason = "identity_missing";
|
|
1512
|
+
} else {
|
|
1513
|
+
struct stat st;
|
|
1514
|
+
if (fstatat(tok->dir_fd, tok->name, &st, AT_SYMLINK_NOFOLLOW) != 0) {
|
|
1515
|
+
removed = false;
|
|
1516
|
+
reason = "missing";
|
|
1517
|
+
} else if (!S_ISREG(st.st_mode) || st.st_dev != tok->tmp_dev ||
|
|
1518
|
+
st.st_ino != tok->tmp_ino) {
|
|
1519
|
+
removed = false;
|
|
1520
|
+
reason = "identity_mismatch";
|
|
1521
|
+
} else if (st.st_nlink != 1) {
|
|
1522
|
+
removed = false;
|
|
1523
|
+
reason = "nlink_gt_1";
|
|
1524
|
+
} else {
|
|
1525
|
+
// Identity matches and nlink==1 — attempt unlink.
|
|
1526
|
+
// If unlink fails, this is a fatal error: the temp file exists with
|
|
1527
|
+
// matching identity but cannot be removed. Throw an error rather than
|
|
1528
|
+
// silently returning removed=false.
|
|
1529
|
+
bool unlink_ok = false;
|
|
1530
|
+
#ifdef SAFE_WRITE_TEST_SEAM
|
|
1531
|
+
if (TestSeamShouldFail(kSeamAbortUnlink)) {
|
|
1532
|
+
// Simulate EACCES from unlinkat
|
|
1533
|
+
errno = EACCES;
|
|
1534
|
+
} else if (unlinkat(tok->dir_fd, tok->name, 0) == 0) {
|
|
1535
|
+
unlink_ok = true;
|
|
1536
|
+
}
|
|
1537
|
+
#else
|
|
1538
|
+
if (unlinkat(tok->dir_fd, tok->name, 0) == 0) {
|
|
1539
|
+
unlink_ok = true;
|
|
1540
|
+
}
|
|
1541
|
+
#endif
|
|
1542
|
+
if (unlink_ok) {
|
|
1543
|
+
removed = true;
|
|
1544
|
+
reason = "removed";
|
|
1545
|
+
} else {
|
|
1546
|
+
// Do NOT consume the token — throw without closing fds or marking
|
|
1547
|
+
// inactive. The caller may reset the seam and retry with the same
|
|
1548
|
+
// token. The temp file remains on disk (identity verified).
|
|
1549
|
+
return ThrowErrno(env, "unlinkat", errno, tok->name);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// Consume token (close fds)
|
|
1555
|
+
tok->active = false;
|
|
1556
|
+
SafeClose(tok->tmp_fd);
|
|
1557
|
+
tok->tmp_fd = -1;
|
|
1558
|
+
SafeClose(tok->dir_fd);
|
|
1559
|
+
tok->dir_fd = -1;
|
|
1560
|
+
|
|
1561
|
+
// Build return object: { removed, reason }
|
|
1562
|
+
napi_value result;
|
|
1563
|
+
NAPI_CHECK(napi_create_object(env, &result));
|
|
1564
|
+
|
|
1565
|
+
napi_value removed_val, reason_val;
|
|
1566
|
+
NAPI_CHECK(napi_get_boolean(env, removed, &removed_val));
|
|
1567
|
+
NAPI_CHECK(napi_create_string_utf8(env, reason, NAPI_AUTO_LENGTH, &reason_val));
|
|
1568
|
+
NAPI_CHECK(napi_set_named_property(env, result, "removed", removed_val));
|
|
1569
|
+
NAPI_CHECK(napi_set_named_property(env, result, "reason", reason_val));
|
|
1570
|
+
return result;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// ---------------------------------------------------------------------------
|
|
1574
|
+
// mkdir(handle, name, mode) — fd-relative via mkdirat (exclusive)
|
|
1575
|
+
//
|
|
1576
|
+
// Creates a directory with the given name and mode inside the parent handle.
|
|
1577
|
+
// Exclusive: EEXIST if the entry already exists (no overwrite).
|
|
1578
|
+
// mkdirat does not follow symlinks by design (POSIX).
|
|
1579
|
+
// After success, fsync the parent directory for durability.
|
|
1580
|
+
// ---------------------------------------------------------------------------
|
|
1581
|
+
|
|
1582
|
+
static napi_value Mkdir(napi_env env, napi_callback_info info) {
|
|
1583
|
+
size_t argc = 4;
|
|
1584
|
+
napi_value argv[4];
|
|
1585
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
1586
|
+
|
|
1587
|
+
int dir_fd = GetDirFd(env, argv[0]);
|
|
1588
|
+
if (dir_fd < 0)
|
|
1589
|
+
return ThrowCode(env, "PATH_UNSAFE", "handle is closed");
|
|
1590
|
+
|
|
1591
|
+
if (argc < 3)
|
|
1592
|
+
return ThrowCode(env, "PATH_UNSAFE", "mkdir requires name and mode");
|
|
1593
|
+
|
|
1594
|
+
auto seg = GetSegment(env, argv, argc, 1);
|
|
1595
|
+
if (!seg.ok)
|
|
1596
|
+
return ThrowCode(env, "PATH_UNSAFE", "mkdir: unsafe segment");
|
|
1597
|
+
|
|
1598
|
+
// Validate mode type (must be int32)
|
|
1599
|
+
{
|
|
1600
|
+
napi_valuetype mt;
|
|
1601
|
+
NAPI_CHECK(napi_typeof(env, argv[2], &mt));
|
|
1602
|
+
if (mt != napi_number) {
|
|
1603
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1604
|
+
"mkdir: mode must be a number");
|
|
1605
|
+
}
|
|
1606
|
+
double dval = 0;
|
|
1607
|
+
NAPI_CHECK(napi_get_value_double(env, argv[2], &dval));
|
|
1608
|
+
if (dval != dval || dval == INFINITY) {
|
|
1609
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1610
|
+
"mkdir: mode must be an integer");
|
|
1611
|
+
}
|
|
1612
|
+
if (static_cast<double>(static_cast<int32_t>(dval)) != dval) {
|
|
1613
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1614
|
+
"mkdir: mode must be an integer");
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
int32_t mode = 0;
|
|
1619
|
+
if (!GetInt32(env, argv[2], mode))
|
|
1620
|
+
return nullptr;
|
|
1621
|
+
if (mode < 0 || mode > 0777) {
|
|
1622
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1623
|
+
"mkdir: mode must be 0..0777");
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// mkdirat: exclusive (EEXIST if entry already exists), does not follow symlinks
|
|
1627
|
+
if (mkdirat(dir_fd, seg.value.c_str(), static_cast<mode_t>(mode)) < 0) {
|
|
1628
|
+
return ThrowErrno(env, "mkdirat", errno, seg.value.c_str());
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// Fsync parent directory for durability
|
|
1632
|
+
int fsync_rc;
|
|
1633
|
+
do {
|
|
1634
|
+
fsync_rc = fsync(dir_fd);
|
|
1635
|
+
} while (fsync_rc < 0 && errno == EINTR);
|
|
1636
|
+
if (fsync_rc < 0) {
|
|
1637
|
+
int saved = errno;
|
|
1638
|
+
// mkdir succeeded but directory not synced — return error but do not
|
|
1639
|
+
// attempt rollback (rmdir) since the directory is now visible.
|
|
1640
|
+
return ThrowErrno(env, "fsync", saved, "<dir>");
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
napi_value undefined;
|
|
1644
|
+
NAPI_CHECK(napi_get_undefined(env, &undefined));
|
|
1645
|
+
return undefined;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// ---------------------------------------------------------------------------
|
|
1649
|
+
// rmdir(handle, name) — fd-relative via unlinkat AT_REMOVEDIR
|
|
1650
|
+
//
|
|
1651
|
+
// Removes an empty directory. The entry must be a directory.
|
|
1652
|
+
// After success, fsync the parent directory for durability.
|
|
1653
|
+
// ---------------------------------------------------------------------------
|
|
1654
|
+
|
|
1655
|
+
static napi_value Rmdir(napi_env env, napi_callback_info info) {
|
|
1656
|
+
size_t argc = 4;
|
|
1657
|
+
napi_value argv[4];
|
|
1658
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
1659
|
+
|
|
1660
|
+
int dir_fd = GetDirFd(env, argv[0]);
|
|
1661
|
+
if (dir_fd < 0)
|
|
1662
|
+
return ThrowCode(env, "PATH_UNSAFE", "handle is closed");
|
|
1663
|
+
|
|
1664
|
+
auto seg = GetSegment(env, argv, argc, 1);
|
|
1665
|
+
if (!seg.ok)
|
|
1666
|
+
return ThrowCode(env, "PATH_UNSAFE", "rmdir: unsafe segment");
|
|
1667
|
+
|
|
1668
|
+
if (unlinkat(dir_fd, seg.value.c_str(), AT_REMOVEDIR) < 0) {
|
|
1669
|
+
return ThrowErrno(env, "unlinkat(AT_REMOVEDIR)", errno, seg.value.c_str());
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// Fsync parent directory for durability
|
|
1673
|
+
int fsync_rc;
|
|
1674
|
+
do {
|
|
1675
|
+
fsync_rc = fsync(dir_fd);
|
|
1676
|
+
} while (fsync_rc < 0 && errno == EINTR);
|
|
1677
|
+
if (fsync_rc < 0) {
|
|
1678
|
+
int saved = errno;
|
|
1679
|
+
return ThrowErrno(env, "fsync", saved, "<dir>");
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
napi_value undefined;
|
|
1683
|
+
NAPI_CHECK(napi_get_undefined(env, &undefined));
|
|
1684
|
+
return undefined;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// ---------------------------------------------------------------------------
|
|
1688
|
+
// unlink(handle, name)
|
|
1689
|
+
// ---------------------------------------------------------------------------
|
|
1690
|
+
|
|
1691
|
+
static napi_value Unlink(napi_env env, napi_callback_info info) {
|
|
1692
|
+
size_t argc = 4;
|
|
1693
|
+
napi_value argv[4];
|
|
1694
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
1695
|
+
|
|
1696
|
+
int dir_fd = GetDirFd(env, argv[0]);
|
|
1697
|
+
if (dir_fd < 0)
|
|
1698
|
+
return ThrowCode(env, "PATH_UNSAFE", "handle is closed");
|
|
1699
|
+
|
|
1700
|
+
auto seg = GetSegment(env, argv, argc, 1);
|
|
1701
|
+
if (!seg.ok)
|
|
1702
|
+
return ThrowCode(env, "PATH_UNSAFE", "unlink: unsafe segment");
|
|
1703
|
+
|
|
1704
|
+
if (unlinkat(dir_fd, seg.value.c_str(), 0) < 0) {
|
|
1705
|
+
return ThrowErrno(env, "unlinkat", errno, seg.value.c_str());
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
napi_value undefined;
|
|
1709
|
+
NAPI_CHECK(napi_get_undefined(env, &undefined));
|
|
1710
|
+
return undefined;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// ---------------------------------------------------------------------------
|
|
1714
|
+
// chmod(handle, name, mode) — fd-based via openat + fchmod (B5)
|
|
1715
|
+
// ---------------------------------------------------------------------------
|
|
1716
|
+
|
|
1717
|
+
static napi_value Chmod(napi_env env, napi_callback_info info) {
|
|
1718
|
+
size_t argc = 4;
|
|
1719
|
+
napi_value argv[4];
|
|
1720
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
1721
|
+
|
|
1722
|
+
int dir_fd = GetDirFd(env, argv[0]);
|
|
1723
|
+
if (dir_fd < 0)
|
|
1724
|
+
return ThrowCode(env, "PATH_UNSAFE", "handle is closed");
|
|
1725
|
+
|
|
1726
|
+
if (argc < 3)
|
|
1727
|
+
return ThrowCode(env, "PATH_UNSAFE", "chmod requires name and mode");
|
|
1728
|
+
|
|
1729
|
+
auto seg = GetSegment(env, argv, argc, 1);
|
|
1730
|
+
if (!seg.ok)
|
|
1731
|
+
return ThrowCode(env, "PATH_UNSAFE", "chmod: unsafe segment");
|
|
1732
|
+
|
|
1733
|
+
// Validate mode type (must be int32)
|
|
1734
|
+
{
|
|
1735
|
+
napi_valuetype mt;
|
|
1736
|
+
NAPI_CHECK(napi_typeof(env, argv[2], &mt));
|
|
1737
|
+
if (mt != napi_number) {
|
|
1738
|
+
return ThrowCode(env, "PATH_UNSAFE", "chmod: mode must be a number");
|
|
1739
|
+
}
|
|
1740
|
+
// Reject NaN and non-integer doubles
|
|
1741
|
+
double dval = 0;
|
|
1742
|
+
NAPI_CHECK(napi_get_value_double(env, argv[2], &dval));
|
|
1743
|
+
if (dval != dval || dval == INFINITY) {
|
|
1744
|
+
return ThrowCode(env, "PATH_UNSAFE", "chmod: mode must be an integer");
|
|
1745
|
+
}
|
|
1746
|
+
if (static_cast<double>(static_cast<int32_t>(dval)) != dval) {
|
|
1747
|
+
return ThrowCode(env, "PATH_UNSAFE", "chmod: mode must be an integer");
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
int32_t mode = 0;
|
|
1752
|
+
if (!GetInt32(env, argv[2], mode))
|
|
1753
|
+
return nullptr;
|
|
1754
|
+
|
|
1755
|
+
// Validate mode range 0..0777 (same as createTemp)
|
|
1756
|
+
if (mode < 0 || mode > 0777) {
|
|
1757
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1758
|
+
"chmod: mode must be 0..0777");
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// Open file to get an fd — fd-based fchmod prevents symlink races (B5)
|
|
1762
|
+
int file_fd = openat(dir_fd, seg.value.c_str(),
|
|
1763
|
+
O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
|
|
1764
|
+
if (file_fd < 0)
|
|
1765
|
+
return ThrowErrno(env, "openat", errno, seg.value.c_str());
|
|
1766
|
+
|
|
1767
|
+
// Verify it's a regular file
|
|
1768
|
+
struct stat st;
|
|
1769
|
+
if (fstat(file_fd, &st) < 0) {
|
|
1770
|
+
int saved = errno;
|
|
1771
|
+
SafeClose(file_fd);
|
|
1772
|
+
return ThrowErrno(env, "fstat", saved, seg.value.c_str());
|
|
1773
|
+
}
|
|
1774
|
+
if (!S_ISREG(st.st_mode) || st.st_nlink != 1) {
|
|
1775
|
+
SafeClose(file_fd);
|
|
1776
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1777
|
+
"chmod: not a regular file or nlink!=1");
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// Use fd-based fchmod
|
|
1781
|
+
if (fchmod(file_fd, mode) < 0) {
|
|
1782
|
+
int saved = errno;
|
|
1783
|
+
SafeClose(file_fd);
|
|
1784
|
+
return ThrowErrno(env, "fchmod", saved, seg.value.c_str());
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
SafeClose(file_fd);
|
|
1788
|
+
|
|
1789
|
+
napi_value undefined;
|
|
1790
|
+
NAPI_CHECK(napi_get_undefined(env, &undefined));
|
|
1791
|
+
return undefined;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// ---------------------------------------------------------------------------
|
|
1795
|
+
// fsync(handle)
|
|
1796
|
+
// ---------------------------------------------------------------------------
|
|
1797
|
+
|
|
1798
|
+
static napi_value Fsync(napi_env env, napi_callback_info info) {
|
|
1799
|
+
size_t argc = 1;
|
|
1800
|
+
napi_value argv[1];
|
|
1801
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
1802
|
+
|
|
1803
|
+
int dir_fd = GetDirFd(env, argv[0]);
|
|
1804
|
+
if (dir_fd < 0)
|
|
1805
|
+
return ThrowCode(env, "PATH_UNSAFE", "handle is closed");
|
|
1806
|
+
|
|
1807
|
+
int rc;
|
|
1808
|
+
do {
|
|
1809
|
+
rc = fsync(dir_fd);
|
|
1810
|
+
} while (rc < 0 && errno == EINTR);
|
|
1811
|
+
if (rc < 0) {
|
|
1812
|
+
return ThrowErrno(env, "fsync", errno, "<dir>");
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
napi_value undefined;
|
|
1816
|
+
NAPI_CHECK(napi_get_undefined(env, &undefined));
|
|
1817
|
+
return undefined;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// ---------------------------------------------------------------------------
|
|
1821
|
+
// close(handle) — idempotent, no EINTR retry
|
|
1822
|
+
// ---------------------------------------------------------------------------
|
|
1823
|
+
|
|
1824
|
+
static napi_value Close(napi_env env, napi_callback_info info) {
|
|
1825
|
+
size_t argc = 1;
|
|
1826
|
+
napi_value argv[1];
|
|
1827
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
1828
|
+
|
|
1829
|
+
int dir_fd = GetDirFd(env, argv[0]);
|
|
1830
|
+
if (dir_fd >= 0) {
|
|
1831
|
+
napi_value ext;
|
|
1832
|
+
NAPI_CHECK(napi_get_named_property(env, argv[0], "_handle", &ext));
|
|
1833
|
+
void* data = nullptr;
|
|
1834
|
+
NAPI_CHECK(napi_get_value_external(env, ext, &data));
|
|
1835
|
+
if (data) {
|
|
1836
|
+
int* fd_ptr = static_cast<int*>(data);
|
|
1837
|
+
SafeClose(*fd_ptr);
|
|
1838
|
+
*fd_ptr = -1;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
napi_value undefined;
|
|
1843
|
+
NAPI_CHECK(napi_get_undefined(env, &undefined));
|
|
1844
|
+
return undefined;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// ---------------------------------------------------------------------------
|
|
1848
|
+
// Module initialization
|
|
1849
|
+
// ---------------------------------------------------------------------------
|
|
1850
|
+
|
|
1851
|
+
#define DEF_FN(name, fn) \
|
|
1852
|
+
do { \
|
|
1853
|
+
napi_value _fn; \
|
|
1854
|
+
NAPI_CHECK(napi_create_function(env, name, NAPI_AUTO_LENGTH, fn, nullptr, \
|
|
1855
|
+
&_fn)); \
|
|
1856
|
+
NAPI_CHECK(napi_set_named_property(env, exports, name, _fn)); \
|
|
1857
|
+
} while (0)
|
|
1858
|
+
|
|
1859
|
+
// ---------------------------------------------------------------------------
|
|
1860
|
+
// Test seam JavaScript interface
|
|
1861
|
+
// Only available in test builds (SAFE_WRITE_TEST_SEAM=1).
|
|
1862
|
+
// ---------------------------------------------------------------------------
|
|
1863
|
+
|
|
1864
|
+
#ifdef SAFE_WRITE_TEST_SEAM
|
|
1865
|
+
|
|
1866
|
+
// testSeamReset() — reset all test seam state.
|
|
1867
|
+
static napi_value TestSeamResetJs(napi_env env, napi_callback_info /*info*/) {
|
|
1868
|
+
TestSeamReset();
|
|
1869
|
+
napi_value undefined;
|
|
1870
|
+
NAPI_CHECK(napi_get_undefined(env, &undefined));
|
|
1871
|
+
return undefined;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// testSeamConfigure(point, count) — configure a failure point.
|
|
1875
|
+
// point: string name of the failure point.
|
|
1876
|
+
// count: -1 = fail every time, 0 = don't fail, N = fail N times.
|
|
1877
|
+
static napi_value TestSeamConfigureJs(napi_env env, napi_callback_info info) {
|
|
1878
|
+
size_t argc = 2;
|
|
1879
|
+
napi_value argv[2];
|
|
1880
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
1881
|
+
|
|
1882
|
+
if (argc < 2) {
|
|
1883
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1884
|
+
"testSeamConfigure requires point and count");
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// Get point string
|
|
1888
|
+
napi_valuetype t;
|
|
1889
|
+
NAPI_CHECK(napi_typeof(env, argv[0], &t));
|
|
1890
|
+
if (t != napi_string) {
|
|
1891
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1892
|
+
"testSeamConfigure: point must be a string");
|
|
1893
|
+
}
|
|
1894
|
+
char point[256];
|
|
1895
|
+
size_t point_len = 0;
|
|
1896
|
+
NAPI_CHECK(napi_get_value_string_utf8(env, argv[0], point, sizeof(point),
|
|
1897
|
+
&point_len));
|
|
1898
|
+
|
|
1899
|
+
// Get count
|
|
1900
|
+
NAPI_CHECK(napi_typeof(env, argv[1], &t));
|
|
1901
|
+
if (t != napi_number) {
|
|
1902
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1903
|
+
"testSeamConfigure: count must be a number");
|
|
1904
|
+
}
|
|
1905
|
+
int32_t count = 0;
|
|
1906
|
+
NAPI_CHECK(napi_get_value_int32(env, argv[1], &count));
|
|
1907
|
+
|
|
1908
|
+
TestSeamConfigure(point, count);
|
|
1909
|
+
|
|
1910
|
+
napi_value undefined;
|
|
1911
|
+
NAPI_CHECK(napi_get_undefined(env, &undefined));
|
|
1912
|
+
return undefined;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// testSeamGetFailedCount(point) — get the number of times a failure point was triggered.
|
|
1916
|
+
static napi_value TestSeamGetFailedCountJs(napi_env env,
|
|
1917
|
+
napi_callback_info info) {
|
|
1918
|
+
size_t argc = 1;
|
|
1919
|
+
napi_value argv[1];
|
|
1920
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
1921
|
+
|
|
1922
|
+
if (argc < 1) {
|
|
1923
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1924
|
+
"testSeamGetFailedCount requires point");
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// Get point string
|
|
1928
|
+
napi_valuetype t;
|
|
1929
|
+
NAPI_CHECK(napi_typeof(env, argv[0], &t));
|
|
1930
|
+
if (t != napi_string) {
|
|
1931
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1932
|
+
"testSeamGetFailedCount: point must be a string");
|
|
1933
|
+
}
|
|
1934
|
+
char point[256];
|
|
1935
|
+
size_t point_len = 0;
|
|
1936
|
+
NAPI_CHECK(napi_get_value_string_utf8(env, argv[0], point, sizeof(point),
|
|
1937
|
+
&point_len));
|
|
1938
|
+
|
|
1939
|
+
int count = TestSeamGetFailedCount(point);
|
|
1940
|
+
|
|
1941
|
+
napi_value result;
|
|
1942
|
+
NAPI_CHECK(napi_create_int32(env, count, &result));
|
|
1943
|
+
return result;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// testSeamGetCallCount(point) — get the number of times a failure point was called.
|
|
1947
|
+
static napi_value TestSeamGetCallCountJs(napi_env env,
|
|
1948
|
+
napi_callback_info info) {
|
|
1949
|
+
size_t argc = 1;
|
|
1950
|
+
napi_value argv[1];
|
|
1951
|
+
NAPI_CHECK(napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
|
|
1952
|
+
|
|
1953
|
+
if (argc < 1) {
|
|
1954
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1955
|
+
"testSeamGetCallCount requires point");
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// Get point string
|
|
1959
|
+
napi_valuetype t;
|
|
1960
|
+
NAPI_CHECK(napi_typeof(env, argv[0], &t));
|
|
1961
|
+
if (t != napi_string) {
|
|
1962
|
+
return ThrowCode(env, "PATH_UNSAFE",
|
|
1963
|
+
"testSeamGetCallCount: point must be a string");
|
|
1964
|
+
}
|
|
1965
|
+
char point[256];
|
|
1966
|
+
size_t point_len = 0;
|
|
1967
|
+
NAPI_CHECK(napi_get_value_string_utf8(env, argv[0], point, sizeof(point),
|
|
1968
|
+
&point_len));
|
|
1969
|
+
|
|
1970
|
+
int count = TestSeamGetCallCount(point);
|
|
1971
|
+
|
|
1972
|
+
napi_value result;
|
|
1973
|
+
NAPI_CHECK(napi_create_int32(env, count, &result));
|
|
1974
|
+
return result;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// testSeamGetPoints() — get all available failure point names.
|
|
1978
|
+
static napi_value TestSeamGetPointsJs(napi_env env,
|
|
1979
|
+
napi_callback_info /*info*/) {
|
|
1980
|
+
// List of all failure point names.
|
|
1981
|
+
static const char* points[] = {
|
|
1982
|
+
kSeamCreateObject, kSeamCreateString,
|
|
1983
|
+
kSeamCreateExternal, kSeamSetProperty,
|
|
1984
|
+
kSeamTypeTag, kSeamOpenRootCreateObject,
|
|
1985
|
+
kSeamOpenDirCreateObject, kSeamReadFileCreateObject,
|
|
1986
|
+
kSeamAbortTempCreateObject, kSeamAbortUnlink,
|
|
1987
|
+
};
|
|
1988
|
+
static const size_t num_points = sizeof(points) / sizeof(points[0]);
|
|
1989
|
+
|
|
1990
|
+
napi_value result;
|
|
1991
|
+
NAPI_CHECK(napi_create_array_with_length(env, num_points, &result));
|
|
1992
|
+
|
|
1993
|
+
for (size_t i = 0; i < num_points; i++) {
|
|
1994
|
+
napi_value str;
|
|
1995
|
+
NAPI_CHECK(
|
|
1996
|
+
napi_create_string_utf8(env, points[i], NAPI_AUTO_LENGTH, &str));
|
|
1997
|
+
NAPI_CHECK(napi_set_element(env, result, i, str));
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
return result;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
#endif // SAFE_WRITE_TEST_SEAM
|
|
2004
|
+
|
|
2005
|
+
static napi_value Init(napi_env env, napi_value exports) {
|
|
2006
|
+
DEF_FN("openRoot", OpenRoot);
|
|
2007
|
+
DEF_FN("openDir", OpenDir);
|
|
2008
|
+
DEF_FN("readEntry", ReadEntry);
|
|
2009
|
+
DEF_FN("readFile", ReadFile);
|
|
2010
|
+
DEF_FN("createTemp", CreateTemp);
|
|
2011
|
+
DEF_FN("rename", Rename);
|
|
2012
|
+
DEF_FN("abortTemp", AbortTemp);
|
|
2013
|
+
DEF_FN("mkdir", Mkdir);
|
|
2014
|
+
DEF_FN("rmdir", Rmdir);
|
|
2015
|
+
DEF_FN("unlink", Unlink);
|
|
2016
|
+
DEF_FN("chmod", Chmod);
|
|
2017
|
+
DEF_FN("fsync", Fsync);
|
|
2018
|
+
DEF_FN("close", Close);
|
|
2019
|
+
|
|
2020
|
+
#ifdef SAFE_WRITE_TEST_SEAM
|
|
2021
|
+
// Test seam functions — only available in test builds.
|
|
2022
|
+
DEF_FN("testSeamReset", TestSeamResetJs);
|
|
2023
|
+
DEF_FN("testSeamConfigure", TestSeamConfigureJs);
|
|
2024
|
+
DEF_FN("testSeamGetFailedCount", TestSeamGetFailedCountJs);
|
|
2025
|
+
DEF_FN("testSeamGetCallCount", TestSeamGetCallCountJs);
|
|
2026
|
+
DEF_FN("testSeamGetPoints", TestSeamGetPointsJs);
|
|
2027
|
+
#endif
|
|
2028
|
+
|
|
2029
|
+
return exports;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
NAPI_MODULE(safe_write, Init)
|