@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.
- package/CHANGELOG.md +44 -0
- package/CLAUDE.md +13 -0
- package/binding.gyp +1 -0
- package/claude.sh +29 -5
- package/dist/index.cjs +237 -129
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -3
- package/dist/index.d.mts +55 -3
- package/dist/index.d.ts +55 -3
- package/dist/index.mjs +236 -130
- package/dist/index.mjs.map +1 -1
- package/doc/SECURITY_AUDIT_2025.md +1 -1
- package/doc/SECURITY_AUDIT_2026.md +361 -0
- package/doc/TPP-GUIDE.md +144 -0
- package/doc/system-volume-detection.md +268 -0
- package/package.json +12 -12
- package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
- package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
- package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/src/binding.cpp +11 -0
- package/src/common/volume_metadata.h +10 -3
- package/src/common/volume_mount_points.h +7 -1
- package/src/darwin/da_mutex.h +23 -0
- package/src/darwin/get_mount_point.cpp +96 -0
- package/src/darwin/get_mount_point.h +13 -0
- package/src/darwin/raii_utils.h +39 -0
- package/src/darwin/system_volume.h +156 -0
- package/src/darwin/volume_metadata.cpp +18 -2
- package/src/darwin/volume_mount_points.cpp +46 -14
- package/src/index.ts +49 -0
- package/src/linux/mtab.ts +6 -0
- package/src/mount_point_for_path.ts +54 -0
- package/src/options.ts +7 -17
- package/src/path.ts +16 -1
- package/src/system_volume.ts +5 -9
- package/src/test-utils/assert.ts +4 -0
- package/src/types/mount_point.ts +28 -1
- package/src/types/native_bindings.ts +7 -0
- package/src/volume_metadata.ts +117 -2
- package/src/windows/system_volume.h +21 -16
- package/src/windows/volume_metadata.cpp +13 -7
- 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**:
|
|
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)
|
package/doc/TPP-GUIDE.md
ADDED
|
@@ -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
|