@photostructure/fs-metadata 1.0.1 → 1.2.0

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/CLAUDE.md +13 -0
  3. package/binding.gyp +1 -0
  4. package/claude.sh +29 -5
  5. package/dist/index.cjs +237 -129
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +55 -3
  8. package/dist/index.d.mts +55 -3
  9. package/dist/index.d.ts +55 -3
  10. package/dist/index.mjs +236 -130
  11. package/dist/index.mjs.map +1 -1
  12. package/doc/SECURITY_AUDIT_2025.md +1 -1
  13. package/doc/SECURITY_AUDIT_2026.md +361 -0
  14. package/doc/TPP-GUIDE.md +144 -0
  15. package/doc/system-volume-detection.md +268 -0
  16. package/package.json +12 -12
  17. package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  18. package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
  19. package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  20. package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
  21. package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
  22. package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
  23. package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  24. package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
  25. package/src/binding.cpp +11 -0
  26. package/src/common/volume_metadata.h +10 -3
  27. package/src/common/volume_mount_points.h +7 -1
  28. package/src/darwin/da_mutex.h +23 -0
  29. package/src/darwin/get_mount_point.cpp +96 -0
  30. package/src/darwin/get_mount_point.h +13 -0
  31. package/src/darwin/raii_utils.h +39 -0
  32. package/src/darwin/system_volume.h +156 -0
  33. package/src/darwin/volume_metadata.cpp +18 -2
  34. package/src/darwin/volume_mount_points.cpp +46 -14
  35. package/src/index.ts +49 -0
  36. package/src/linux/mtab.ts +6 -0
  37. package/src/mount_point_for_path.ts +54 -0
  38. package/src/options.ts +7 -17
  39. package/src/path.ts +16 -1
  40. package/src/system_volume.ts +5 -9
  41. package/src/test-utils/assert.ts +4 -0
  42. package/src/types/mount_point.ts +28 -1
  43. package/src/types/native_bindings.ts +7 -0
  44. package/src/volume_metadata.ts +117 -2
  45. package/src/windows/system_volume.h +21 -16
  46. package/src/windows/volume_metadata.cpp +13 -7
  47. package/src/windows/volume_mount_points.cpp +11 -7
@@ -766,7 +766,7 @@ describe("Security: Path Validation", () => {
766
766
  ## Document Maintenance
767
767
 
768
768
  **Last Updated**: December 28, 2025
769
- **Next Review**: June 2026 (or after major dependency updates)
769
+ **Next Review**: Completed March 2026 see `doc/SECURITY_AUDIT_2026.md`
770
770
 
771
771
  **Change Log**:
772
772
 
@@ -0,0 +1,361 @@
1
+ # Security Audit Report - March 17, 2026
2
+
3
+ **Project**: @photostructure/fs-metadata
4
+ **Auditor**: Claude (Anthropic)
5
+ **Scope**: Complete codebase review including API verification against official documentation
6
+ **Previous Audit**: October-December 2025 (see `doc/SECURITY_AUDIT_2025.md`)
7
+
8
+ ## Executive Summary
9
+
10
+ This audit covers all changes since the December 2025 re-audit, focusing on the new
11
+ macOS system volume detection system (APFS volume roles via IOKit/DiskArbitration),
12
+ the `isReadOnly`/`volumeRole`/`isSystemVolume` fields added to both C++ structs and
13
+ TypeScript interfaces, and updated TypeScript heuristics for container runtimes.
14
+
15
+ **Overall Security Rating: A (Excellent)** _(All findings resolved during this audit)_
16
+
17
+ ### Codebase Reviewed
18
+
19
+ - 27 C++ files (11 headers, 16 source files) across `src/common/`, `src/darwin/`, `src/windows/`, `src/linux/`
20
+ - ~30 TypeScript source files, 43 test files
21
+ - `binding.gyp` build configuration
22
+
23
+ ### Key Changes Since December 2025
24
+
25
+ 1. **New**: macOS APFS volume role detection via IOKit (`system_volume.h`)
26
+ 2. **New**: `ClassifyMacVolume()` combining `MNT_SNAPSHOT` + `MNT_DONTBROWSE` + APFS roles
27
+ 3. **New**: `isReadOnly`, `volumeRole` fields on `MountPoint` and `VolumeMetadata`
28
+ 4. **Updated**: `DASessionRAII` usage in both `volume_metadata.cpp` and `volume_mount_points.cpp`
29
+ 5. **Updated**: TypeScript system path patterns expanded for container runtimes
30
+ 6. **Updated**: `assignSystemVolume()` never downgrades native `isSystemVolume=true`
31
+
32
+ ### Strengths (Carried Forward)
33
+
34
+ - ✅ Comprehensive RAII patterns on all platforms
35
+ - ✅ Path validation with `realpath()` (POSIX) and `PathCchCanonicalizeEx` (Windows)
36
+ - ✅ Null byte injection and directory traversal prevention at both C++ and TypeScript layers
37
+ - ✅ Integer overflow protection in volume size calculations
38
+ - ✅ File descriptor-based operations preventing TOCTOU
39
+ - ✅ Strong compiler security flags on all platforms
40
+ - ✅ No unsafe C functions (`strcpy`, `sprintf`, `gets`, etc.)
41
+ - ✅ CodeQL, Snyk, and ESLint security plugin in CI
42
+
43
+ ---
44
+
45
+ ## Findings
46
+
47
+ ### Finding #1: IOKit Objects Not RAII-Wrapped in GetApfsVolumeRole() ✅ FIXED
48
+
49
+ **Severity**: 🟡 MEDIUM → ✅ RESOLVED
50
+ **CWE**: [CWE-404](https://cwe.mitre.org/data/definitions/404.html) (Improper Resource Shutdown or Release)
51
+ **File**: `src/darwin/system_volume.h` (lines 50-94)
52
+
53
+ **Issue**: `io_service_t media` and `io_registry_entry_t parent` were raw IOKit handles
54
+ released manually with `IOObjectRelease()`. If `std::string` construction or any other
55
+ operation between acquisition and release threw a C++ exception (e.g., `std::bad_alloc`),
56
+ `IOObjectRelease()` would never be called, leaking Mach port resources.
57
+
58
+ The `CFReleaser<CFArrayRef>` for `role` in the same function was already correctly
59
+ RAII-wrapped, making this an inconsistency.
60
+
61
+ **Fix Applied**:
62
+
63
+ Created `IOObjectGuard` in `src/darwin/raii_utils.h` — a RAII wrapper for `io_object_t`
64
+ following the same pattern as `CFReleaser`:
65
+
66
+ ```cpp
67
+ class IOObjectGuard {
68
+ io_object_t obj_;
69
+ public:
70
+ explicit IOObjectGuard(io_object_t obj = 0) noexcept : obj_(obj) {}
71
+ ~IOObjectGuard() noexcept { if (obj_) IOObjectRelease(obj_); }
72
+ // non-copyable, movable
73
+ };
74
+ ```
75
+
76
+ Updated `GetApfsVolumeRole()` to use `IOObjectGuard` for both `media` and `parent`.
77
+
78
+ **API Verification**:
79
+
80
+ - [`DADiskCopyIOMedia()`](<https://developer.apple.com/documentation/diskarbitration/dadiskcopymedia(_:)>) — Returns `io_service_t`; caller owns the reference
81
+ - [`IORegistryEntryGetParentEntry()`](https://developer.apple.com/documentation/iokit/1514761-ioregistryentrygetparententry) — Output `io_registry_entry_t`; caller owns the reference
82
+ - [`IOObjectRelease()`](https://developer.apple.com/documentation/iokit/1514627-ioobjectrelease) — Must be called once per owned reference
83
+
84
+ ---
85
+
86
+ ### Finding #2: Missing Mutex for DiskArbitration in volume_mount_points.cpp ✅ FIXED
87
+
88
+ **Severity**: 🟡 MEDIUM → ✅ RESOLVED
89
+ **CWE**: [CWE-362](https://cwe.mitre.org/data/definitions/362.html) (Concurrent Execution using Shared Resource with Improper Synchronization)
90
+ **File**: `src/darwin/volume_mount_points.cpp` (lines 53-88)
91
+
92
+ **Issue**: The October 2025 audit (Finding #5) established that DA operations must be
93
+ serialized via `g_diskArbitrationMutex`. `volume_metadata.cpp` correctly held this mutex
94
+ before any DA calls. However, `volume_mount_points.cpp` — added later as part of the
95
+ system volume detection feature — created its own `DASessionRAII` and called
96
+ `ClassifyMacVolume()` **without any mutex protection**.
97
+
98
+ When `getVolumeMountPoints()` and `getVolumeMetadata()` are called concurrently from
99
+ JavaScript, two AsyncWorker threads could perform DA + IOKit operations simultaneously,
100
+ which Apple's documentation does not explicitly guarantee is safe.
101
+
102
+ **Fix Applied**:
103
+
104
+ 1. Created `src/darwin/da_mutex.h` — shared header declaring `extern std::mutex g_diskArbitrationMutex`
105
+ 2. Changed `volume_metadata.cpp`'s definition from `static` to `extern`-compatible
106
+ 3. Updated `volume_mount_points.cpp` to:
107
+ - Include `da_mutex.h`
108
+ - Perform all DA/IOKit classification under `std::lock_guard<std::mutex>` **before** launching async accessibility checks
109
+ - Release the mutex before the potentially-slow `faccessat()` calls
110
+ 4. Restructured the batching loop so DA operations are fully separated from I/O checks
111
+
112
+ **Design Decision**: All DA + IOKit operations are now batched into a single mutex-protected
113
+ block at the start, rather than interleaved with `faccessat()` checks. This minimizes
114
+ mutex hold time while ensuring complete serialization of framework calls.
115
+
116
+ ---
117
+
118
+ ### Finding #3: Uninitialized `double` Members in VolumeMetadata Struct ✅ FIXED
119
+
120
+ **Severity**: 🟡 MEDIUM → ✅ RESOLVED
121
+ **CWE**: [CWE-457](https://cwe.mitre.org/data/definitions/457.html) (Use of Uninitialized Variable)
122
+ **File**: `src/common/volume_metadata.h` (lines 43-45)
123
+
124
+ **Issue**: The `size`, `used`, and `available` fields were uninitialized:
125
+
126
+ ```cpp
127
+ double size; // NO INITIALIZER
128
+ double used; // NO INITIALIZER
129
+ double available; // NO INITIALIZER
130
+ ```
131
+
132
+ Other members in the same struct had default initializers (`bool remote = false;`,
133
+ `bool isSystemVolume = false;`). If `GetBasicVolumeInfo()` failed early or a Windows
134
+ drive was not `Healthy`, `MetadataWorkerBase::OnOK()` would still call `ToObject()`,
135
+ serializing uninitialized garbage values to JavaScript.
136
+
137
+ **Fix Applied**:
138
+
139
+ ```cpp
140
+ double size = 0.0;
141
+ double used = 0.0;
142
+ double available = 0.0;
143
+ ```
144
+
145
+ ---
146
+
147
+ ### Finding #4: `strerror()` Thread Safety (Informational)
148
+
149
+ **Severity**: 🟢 LOW → NO ACTION NEEDED
150
+ **CWE**: [CWE-362](https://cwe.mitre.org/data/definitions/362.html) (Race Condition)
151
+ **File**: `src/common/error_utils.h` (lines 25, 32)
152
+
153
+ **Issue**: `strerror()` is not guaranteed thread-safe by POSIX (it may return a pointer
154
+ to a static buffer). Used in `CreatePathErrorMessage()` and `CreateDetailedErrorMessage()`,
155
+ called from AsyncWorker threads.
156
+
157
+ **Assessment**: No fix needed at this time.
158
+
159
+ - glibc's `strerror()` is thread-safe (uses thread-local buffer)
160
+ - Apple's `strerror()` is also thread-safe
161
+ - These are the only two POSIX platforms this project supports
162
+ - The code already follows best practice: capturing `errno` immediately into a local `int error` variable, then calling `strerror(error)` (not `strerror(errno)`)
163
+
164
+ **Recommendation**: If Alpine Linux (musl libc) support is added, verify `strerror()` thread safety or switch to `strerror_r()`.
165
+
166
+ ---
167
+
168
+ ### Finding #5: `compileGlob()` Pattern Complexity (Informational)
169
+
170
+ **Severity**: 🟢 LOW → NO ACTION NEEDED
171
+ **File**: `src/glob.ts`
172
+
173
+ **Issue**: `compileGlob()` compiles user-provided `systemPathPatterns` into a `RegExp`.
174
+ A caller could theoretically provide a malicious pattern causing ReDoS.
175
+
176
+ **Assessment**: No fix needed.
177
+
178
+ - The glob-to-regex translation produces simple patterns (no nested quantifiers)
179
+ - Patterns are matched against mount point paths (short, bounded-length strings)
180
+ - The cache is bounded to 256 entries
181
+ - Default patterns are hardcoded constants (`SystemPathPatternsDefault`)
182
+ - This is a library consumed by application code, not a web-facing API
183
+
184
+ ---
185
+
186
+ ### Finding #6: Redundant `GetVolumeInformationW` Calls (Windows, Efficiency) ✅ FIXED
187
+
188
+ **Severity**: 🟢 LOW → ✅ RESOLVED
189
+ **CWE**: N/A (efficiency, not security)
190
+ **Files**: `src/windows/volume_mount_points.cpp`, `src/windows/volume_metadata.cpp`, `src/windows/system_volume.h`
191
+
192
+ **Issue**: `GetVolumeInformationW` was called redundantly:
193
+ - In `volume_mount_points.cpp`: once for `fstype`/`isReadOnly`, then again inside `IsSystemVolume()`
194
+ - In `volume_metadata.cpp`: `IsSystemVolume()` queried the API, then `VolumeInfo` queried it again 5 lines later
195
+
196
+ **Fix Applied**:
197
+
198
+ 1. Added `volumeFlags` parameter to `IsSystemVolume()` (default `0` for backward compat)
199
+ 2. When `volumeFlags != 0`, skips the redundant `GetVolumeInformationW` call
200
+ 3. `volume_mount_points.cpp`: passes pre-fetched `fsFlags` to `IsSystemVolume()`, and
201
+ moves the `IsSystemVolume` call inside the healthy-drive block to prevent querying
202
+ dead drives (which would hang the worker thread, defeating async timeout protection)
203
+ 4. `volume_metadata.cpp`: reordered to create `VolumeInfo` first, then pass its flags
204
+ to `IsSystemVolume()` — eliminates the duplicate query
205
+
206
+ ---
207
+
208
+ ## Re-Verification of October-December 2025 Audit Findings
209
+
210
+ | # | Finding | Severity | Status | Verification |
211
+ | --- | --------------------------------------- | -------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
212
+ | 1 | Path Validation Bypass | CRITICAL | ✅ STILL FIXED | `ValidatePathForRead()` with `realpath()` confirmed in `darwin/volume_metadata.cpp:82-87` and `linux/volume_metadata.cpp:41-45`. `path_security.h` unchanged. |
213
+ | 2 | Windows Path Length | CRITICAL | ✅ STILL FIXED | `PathCchCanonicalizeEx` with `PATHCCH_ALLOW_LONG_PATHS` in `security_utils.h:112-115`. |
214
+ | 3 | Integer Overflow in String Conversion | CRITICAL | ✅ STILL FIXED | `SafeStringToWide` with `MB_ERR_INVALID_CHARS` in `security_utils.h:155-168`. `WideToUtf8` with `INT_MAX`/`MAX_STRING_CONVERSION_SIZE` checks in `string.h:35-40`. |
215
+ | 4 | Memory Leak in Windows Error Formatting | HIGH | ✅ STILL FIXED | `LocalFreeGuard` in `windows/error_utils.h`. |
216
+ | 5 | DiskArbitration Threading | HIGH | ⚠️ PARTIALLY REGRESSED → ✅ RE-FIXED | `volume_metadata.cpp` still used the mutex. New `volume_mount_points.cpp` did not. Fixed in Finding #2 above with shared `da_mutex.h`. |
217
+ | 6 | GVolumeMonitor Thread Safety (Linux) | HIGH | ✅ STILL FIXED | `g_unix_mounts_get()` approach unchanged. |
218
+ | 7 | Double-Free in GIO (Linux) | HIGH | ✅ STILL FIXED | `GUnixMountEntry` approach unchanged. |
219
+ | 8 | CFStringGetCString Error Logging | MEDIUM | ✅ STILL FIXED | Debug logging in `volume_metadata.cpp:54-58`. |
220
+ | 9 | TOCTOU Race Condition | MEDIUM | ✅ STILL FIXED | `open()` + `fstatvfs(fd)` pattern in both darwin and linux `volume_metadata.cpp`. |
221
+ | 10 | blkid Memory Management | MEDIUM | ✅ STILL FIXED | `free()` with documentation in `linux/volume_metadata.cpp:134-156`. |
222
+ | 11 | Thread Pool Timeout | LOW | ✅ STILL ACCEPTABLE | No changes. |
223
+ | 12 | ARM64 Security Flags | LOW | ✅ STILL DOCUMENTED | `binding.gyp` inline comments and `doc/WINDOWS_ARM64_SECURITY.md`. |
224
+
225
+ ---
226
+
227
+ ## New API Verification Matrix
228
+
229
+ All new API calls introduced since the December 2025 audit:
230
+
231
+ | API | Platform | Usage Location | Documentation | Status |
232
+ | ----------------------------------- | -------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------ |
233
+ | `DADiskCopyIOMedia()` | macOS | `system_volume.h:50` | [Apple](<https://developer.apple.com/documentation/diskarbitration/dadiskcopymedia(_:)>) | ✅ Now RAII-wrapped |
234
+ | `IORegistryEntryCreateCFProperty()` | macOS | `system_volume.h:58,68` | [Apple](https://developer.apple.com/documentation/iokit/1514342-ioregistryentrycreatecfproperty) | ✅ Wrapped in `CFReleaser` |
235
+ | `IORegistryEntryGetParentEntry()` | macOS | `system_volume.h:66` | [Apple](https://developer.apple.com/documentation/iokit/1514761-ioregistryentrygetparententry) | ✅ Now RAII-wrapped |
236
+ | `IOObjectRelease()` | macOS | `raii_utils.h` (via `IOObjectGuard`) | [Apple](https://developer.apple.com/documentation/iokit/1514627-ioobjectrelease) | ✅ RAII destructor |
237
+ | `CFArrayGetCount()` | macOS | `system_volume.h:75` | [Apple](https://developer.apple.com/documentation/corefoundation/1388772-cfarraygetcount) | ✅ Bounds-checked |
238
+ | `CFArrayGetValueAtIndex()` | macOS | `system_volume.h:78` | [Apple](https://developer.apple.com/documentation/corefoundation/1388767-cfarraygetvalueatindex) | ✅ Index validated (count > 0) |
239
+ | `CFGetTypeID()` | macOS | `system_volume.h:74,79` | [Apple](https://developer.apple.com/documentation/corefoundation/1521218-cfgettypeid) | ✅ Type-checked before cast |
240
+ | `getmntinfo_r_np()` | macOS | `volume_mount_points.cpp:40` | `man getmntinfo_r_np` | ✅ Thread-safe, RAII buffer |
241
+ | `SHGetFolderPathW()` | Windows | `system_volume.h:24` | [Microsoft](https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetfolderpathw) | ✅ HRESULT checked |
242
+ | `_wcsnicmp()` | Windows | `system_volume.h:29` | [Microsoft](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/strnicmp-wcsnicmp-mbsnicmp-strnicmp-l-wcsnicmp-l-mbsnicmp-l) | ✅ Compares 2 chars only |
243
+ | `wcsncpy_s()` | Windows | `system_volume.h:26` | [Microsoft](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/strncpy-s-strncpy-s-l-wcsncpy-s-wcsncpy-s-l-mbsncpy-s-mbsncpy-s-l) | ✅ 3 chars into WCHAR[4] |
244
+
245
+ ---
246
+
247
+ ## Memory Safety Summary
248
+
249
+ ### RAII Coverage (All Resource Types)
250
+
251
+ | Resource Type | RAII Wrapper | Platform | Status |
252
+ | ---------------------------- | ----------------------------- | ----------- | ----------------------- |
253
+ | CoreFoundation objects | `CFReleaser<T>` | macOS | ✅ Complete |
254
+ | IOKit objects | `IOObjectGuard` | macOS | ✅ **NEW** (this audit) |
255
+ | DASession + dispatch queue | `DASessionRAII` | macOS | ✅ Complete |
256
+ | `getmntinfo_r_np()` buffer | `MountBufferRAII` | macOS | ✅ Complete |
257
+ | `malloc()`-allocated buffers | `ResourceRAII<T>` | macOS | ✅ Complete |
258
+ | POSIX file descriptors | `FdGuard` | macOS/Linux | ✅ Complete |
259
+ | Windows `HANDLE` | `HandleGuard` | Windows | ✅ Complete |
260
+ | `FindFirstFile` handles | `FindHandleGuard` | Windows | ✅ Complete |
261
+ | `CRITICAL_SECTION` | `CriticalSectionGuard` | Windows | ✅ Complete |
262
+ | `FormatMessageA` buffer | `LocalFreeGuard` | Windows | ✅ Complete |
263
+ | `WNetGetConnection` buffer | `WNetConnection` (unique_ptr) | Windows | ✅ Complete |
264
+ | GObject/GFree | `GObjectPtr<T>`, `GCharPtr` | Linux | ✅ Complete |
265
+ | blkid cache | `BlkidCache` | Linux | ✅ Complete |
266
+
267
+ ### Unsafe Function Audit
268
+
269
+ No unsafe C functions found:
270
+
271
+ - ❌ No `strcpy`, `strcat`, `sprintf`, `gets`, `scanf`
272
+ - ❌ No `memcpy` with untrusted sizes
273
+ - ❌ No unvalidated buffer operations
274
+ - ✅ `std::string` used throughout for dynamic strings
275
+ - ✅ `CFStringGetCString` with explicit buffer size
276
+ - ✅ `wcsncpy_s` (Windows secure variant) with size parameter
277
+
278
+ ---
279
+
280
+ ## Compiler Security Flags Verification
281
+
282
+ | Flag | macOS | Linux x64 | Linux ARM64 | Windows x64 | Windows ARM64 |
283
+ | ---------------------- | -------------------------- | -------------------------- | ------------------------------ | ---------------- | -------------- |
284
+ | Stack Protector | `-fstack-protector-strong` | `-fstack-protector-strong` | `-fstack-protector-strong` | `/sdl` | `/sdl` |
285
+ | Source Fortification | `-D_FORTIFY_SOURCE=2` | `-D_FORTIFY_SOURCE=2` | `-D_FORTIFY_SOURCE=2` | N/A | N/A |
286
+ | Format Security | `-Wformat-security` | `-Wformat-security` | `-Wformat-security` | N/A | N/A |
287
+ | Control Flow Integrity | N/A | `-fcf-protection=full` | `-mbranch-protection=standard` | `/guard:cf` | `/guard:cf` |
288
+ | Spectre Mitigation | N/A | N/A | N/A | `/Qspectre` | N/A (HW) |
289
+ | ASLR | default | default | default | `/DYNAMICBASE` | `/DYNAMICBASE` |
290
+ | DEP | default | default | default | `/NXCOMPAT` | `/NXCOMPAT` |
291
+ | High Entropy ASLR | default | default | default | `/HIGHENTROPYVA` | N/A |
292
+ | CET/Shadow Stack | N/A | N/A | N/A | `/CETCOMPAT` | N/A (BTI) |
293
+
294
+ ---
295
+
296
+ ## Thread Safety Summary
297
+
298
+ | Mechanism | Location | Protects |
299
+ | --------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------- |
300
+ | `g_diskArbitrationMutex` | `da_mutex.h` (shared) | All DA + IOKit operations across both metadata and mount point workers |
301
+ | `DASessionRAII` + serial dispatch queue | `volume_metadata.cpp`, `volume_mount_points.cpp` | DA session lifecycle (unschedule-before-release) |
302
+ | `BlkidCache::mutex_` | `blkid_cache.h` | blkid cache operations |
303
+ | `CRITICAL_SECTION` | `thread_pool.h`, `drive_status.h` | Windows thread pool task queue |
304
+ | `Napi::AsyncWorker` | All worker classes | V8 isolate access (N-API guarantee) |
305
+ | `std::async` + `std::future` | `volume_mount_points.cpp` | Timeout-aware concurrent `faccessat()` checks |
306
+
307
+ ---
308
+
309
+ ## Testing Summary
310
+
311
+ - **Test Suite**: 503 tests passed, 55 platform-specific skipped (558 total)
312
+ - **macOS Concurrent Tests**: 100 rapid DA requests (`darwin-disk-arbitration-threading.test.ts`)
313
+ - **Cross-API Concurrent Tests**: Interleaved `getVolumeMountPoints()` + `getVolumeMetadata()` calls
314
+ - **System Volume Detection**: Validated on macOS (`/` = MNT_SNAPSHOT, `/System/Volumes/VM` = APFS role)
315
+ - **Error Handling**: Invalid paths, null inputs, non-existent paths, empty strings
316
+
317
+ ### Recommended Future Testing
318
+
319
+ - ThreadSanitizer build on macOS/Linux for runtime data race detection
320
+ - AddressSanitizer build for memory safety regression testing
321
+ - Cross-API stress test combining `getVolumeMountPoints()` + `getVolumeMetadata()` + `isHidden()` concurrently
322
+
323
+ ---
324
+
325
+ ## References
326
+
327
+ ### Official Documentation Sources
328
+
329
+ - **Windows APIs**: [Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/)
330
+ - **macOS APIs**: [Apple Developer Documentation](https://developer.apple.com/documentation/)
331
+ - **IOKit**: [IOKit Framework Reference](https://developer.apple.com/documentation/iokit)
332
+ - **DiskArbitration**: [DiskArbitration Framework](https://developer.apple.com/documentation/diskarbitration)
333
+ - **Linux System Calls**: [man7.org](https://man7.org/linux/man-pages/)
334
+ - **GIO/GLib**: [GNOME Developer](https://developer.gnome.org/)
335
+ - **libblkid**: [util-linux GitHub](https://github.com/util-linux/util-linux)
336
+
337
+ ### Security Resources
338
+
339
+ - [CWE-362: Race Condition](https://cwe.mitre.org/data/definitions/362.html)
340
+ - [CWE-404: Improper Resource Shutdown or Release](https://cwe.mitre.org/data/definitions/404.html)
341
+ - [CWE-457: Use of Uninitialized Variable](https://cwe.mitre.org/data/definitions/457.html)
342
+ - [Apple Secure Coding Guide: Race Conditions](https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Articles/RaceConditions.html)
343
+
344
+ ---
345
+
346
+ ## Document Maintenance
347
+
348
+ **Created**: March 17, 2026
349
+ **Next Review**: September 2026 (or after major dependency updates)
350
+
351
+ **Change Log**:
352
+
353
+ - 2026-03-17: Initial audit
354
+ - 6 findings identified (3 medium, 3 low)
355
+ - 4 findings fixed during audit (Findings #1, #2, #3, #6)
356
+ - 2 findings assessed as acceptable (Findings #4, #5)
357
+ - All 12 findings from October 2025 audit re-verified
358
+ - Finding #5 from 2025 (DA threading) found partially regressed in new code, re-fixed
359
+ - Created `IOObjectGuard` RAII wrapper for IOKit objects
360
+ - Created `da_mutex.h` for shared DA mutex across translation units
361
+ - Test suite: 503 tests passing (55 platform-specific skipped)
@@ -0,0 +1,144 @@
1
+ # Technical Project Plans (TPPs)
2
+
3
+ TPPs are markdown files that persist research, design decisions, and progress across Claude Code sessions. They solve a specific problem: when a session ends (context limit, crash, task switch), all the accumulated understanding is lost. TPPs capture that understanding so the next session starts informed, not from scratch.
4
+
5
+ ## Why TPPs exist
6
+
7
+ Claude Code has three built-in persistence mechanisms, and all have failure modes:
8
+
9
+ - **CLAUDE.md** — project-wide, not task-specific. Can't hold active task state.
10
+ - **/compact** — lossy compression. Nuance, failed approaches, and partial progress are discarded.
11
+ - **Plan mode** — ephemeral. Gone when the session ends.
12
+
13
+ TPPs fill the gap: task-specific, persistent, and designed for handoff between sessions.
14
+
15
+ ## Directory structure
16
+
17
+ ```
18
+ _todo/ # Active TPPs (work in progress)
19
+ _done/ # Completed TPPs (reference/archive)
20
+ ```
21
+
22
+ TPP filenames use date prefixes for chronological sorting:
23
+
24
+ ```
25
+ _todo/20260316-volume-metadata-refactor.md
26
+ _todo/20260320-linux-gio-support.md
27
+ _done/20260310-hidden-file-root-fix.md
28
+ ```
29
+
30
+ ## TPP template
31
+
32
+ ```markdown
33
+ # TPP: Feature name
34
+
35
+ ## Summary
36
+
37
+ Short description of the problem (under 10 lines).
38
+
39
+ ## Current phase
40
+
41
+ - [x] Research & Planning
42
+ - [x] Write breaking tests
43
+ - [ ] Design alternatives
44
+ - [ ] Task breakdown
45
+ - [ ] Implementation
46
+ - [ ] Review & Refinement
47
+ - [ ] Final Integration
48
+ - [ ] Review
49
+
50
+ ## Required reading
51
+
52
+ Files and docs the engineer must study before starting work.
53
+
54
+ ## Description
55
+
56
+ Detailed context about the problem (under 20 lines).
57
+
58
+ ## Lore
59
+
60
+ - Non-obvious details that will save time
61
+ - Prior gotchas that tripped up previous sessions
62
+ - Relevant functions, classes, and historical context
63
+
64
+ ## Solutions
65
+
66
+ ### Option A (preferred)
67
+
68
+ Description with pros/cons and code snippets if helpful.
69
+
70
+ ### Option B (alternative)
71
+
72
+ Why this was considered and why Option A is better.
73
+
74
+ ## Tasks
75
+
76
+ - [x] Task 1: Clear deliverable, files to change, verification command
77
+ - [ ] Task 2: ...
78
+ ```
79
+
80
+ ## How to use TPPs
81
+
82
+ ### Starting a new task
83
+
84
+ 1. Create a new file in `_todo/` with today's date and a descriptive name
85
+ 2. Fill in the Summary, Description, and Required reading sections
86
+ 3. Use `/tpp _todo/YYYY-MM-DD-name.md` to begin working
87
+
88
+ ### Resuming work
89
+
90
+ 1. Start a new session
91
+ 2. Run `/tpp _todo/YYYY-MM-DD-name.md` — the skill reads the TPP and picks up where the last session left off
92
+
93
+ ### Ending a session
94
+
95
+ When context is running low or you're switching tasks:
96
+
97
+ 1. Run `/handoff` — this updates the TPP with current progress, discoveries, and next steps
98
+ 2. The next session can pick up cold from the updated TPP
99
+
100
+ ### Completing a task
101
+
102
+ When all phases are done:
103
+
104
+ 1. Move the TPP from `_todo/` to `_done/`
105
+ 2. The completed TPP serves as reference for future related work
106
+
107
+ ## Writing good TPPs
108
+
109
+ ### The Lore section is critical
110
+
111
+ This is where you capture things that aren't obvious from the code:
112
+
113
+ - "DiskArbitration callbacks fire on a different thread — must use dispatch queues"
114
+ - "Windows GetVolumeInformation blocks indefinitely on disconnected network drives"
115
+ - "The mtab parser must handle both /etc/mtab and /proc/self/mountinfo formats"
116
+
117
+ ### Document failed approaches
118
+
119
+ When something doesn't work, record it and WHY:
120
+
121
+ - "Tried using statfs for remote detection but it doesn't distinguish NFS subtypes on Linux"
122
+ - "CFURLCopyResourcePropertyForKey returns null for /.vol paths — use getattrlist instead"
123
+
124
+ This prevents the next session from wasting time re-exploring dead ends.
125
+
126
+ ### Tasks must be concrete
127
+
128
+ Bad: "Implement Windows support"
129
+ Good: "Add GetVolumeInformationW call in src/windows/volume_metadata.cpp, handle ERROR_NOT_READY for removable drives, add test case in src/volume_metadata.test.ts"
130
+
131
+ ### Keep it current
132
+
133
+ A stale TPP is worse than no TPP. Update it as you learn things, not just at handoff time.
134
+
135
+ ## Project-specific conventions
136
+
137
+ This project is a cross-platform native Node.js module. TPPs should always consider:
138
+
139
+ 1. **All three platforms** — Windows, macOS, Linux (including Alpine/musl and ARM64)
140
+ 2. **Native + TypeScript** — changes often span both C++ and TS layers
141
+ 3. **RAII everywhere** — no raw resource management in C++ code
142
+ 4. **Backwards compatibility** — this is a published npm package
143
+ 5. **CI reliability** — tests must be deterministic (see CLAUDE.md anti-patterns)
144
+ 6. **Timeouts** — native calls can hang on network filesystems