@photostructure/fs-metadata 0.6.1 → 0.7.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 +7 -1
- package/CLAUDE.md +141 -315
- package/CODE_OF_CONDUCT.md +11 -11
- package/CONTRIBUTING.md +1 -1
- package/README.md +34 -103
- package/binding.gyp +97 -22
- package/claude.sh +23 -0
- package/dist/index.cjs +51 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +51 -21
- package/dist/index.mjs.map +1 -1
- package/doc/C++_REVIEW_TODO.md +97 -25
- package/doc/GPG_RELEASE_HOWTO.md +44 -13
- package/doc/MACOS_API_REFERENCE.md +469 -0
- package/doc/SECURITY_AUDIT_2025.md +809 -0
- package/doc/SSH_RELEASE_HOWTO.md +28 -24
- package/doc/WINDOWS_API_REFERENCE.md +422 -0
- package/doc/WINDOWS_ARM64_SECURITY.md +161 -0
- package/doc/WINDOWS_DEBUG_GUIDE.md +9 -2
- package/doc/examples.md +267 -0
- package/doc/gotchas.md +297 -0
- package/doc/logo.png +0 -0
- package/doc/logo.svg +85 -0
- package/doc/macos-asan-sip-issue.md +71 -0
- package/doc/social.png +0 -0
- package/doc/social.svg +125 -0
- package/doc/windows-build.md +226 -0
- package/doc/windows-clang-tidy.md +72 -0
- package/doc/windows-memory-testing.md +108 -0
- package/doc/windows-prebuildify-arm64.md +232 -0
- package/jest.config.cjs +23 -0
- package/package.json +61 -36
- 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/scripts/check-memory.ts +186 -0
- package/scripts/clang-tidy.ts +690 -99
- package/scripts/install.cjs +42 -0
- package/scripts/is-platform.mjs +1 -1
- package/scripts/macos-asan.sh +155 -0
- package/scripts/post-build.mjs +3 -3
- package/scripts/prebuild-linux-glibc.sh +12 -1
- package/scripts/prebuildify-wrapper.ts +77 -0
- package/scripts/precommit.ts +45 -20
- package/scripts/sanitizers-test.sh +1 -1
- package/src/common/volume_metadata.h +6 -0
- package/src/darwin/hidden.cpp +73 -25
- package/src/darwin/path_security.h +149 -0
- package/src/darwin/raii_utils.h +104 -4
- package/src/darwin/volume_metadata.cpp +132 -58
- package/src/darwin/volume_mount_points.cpp +80 -47
- package/src/hidden.ts +36 -13
- package/src/linux/gio_mount_points.cpp +17 -18
- package/src/linux/gio_utils.cpp +92 -37
- package/src/linux/gio_utils.h +11 -5
- package/src/linux/gio_volume_metadata.cpp +111 -48
- package/src/linux/volume_metadata.cpp +67 -4
- package/src/object.ts +1 -0
- package/src/options.ts +6 -0
- package/src/path.ts +11 -0
- package/src/remote_info.ts +5 -3
- package/src/stack_path.ts +8 -6
- package/src/string_enum.ts +1 -0
- package/src/test-utils/memory-test-core.ts +336 -0
- package/src/test-utils/memory-test-runner.ts +108 -0
- package/src/test-utils/platform.ts +46 -1
- package/src/test-utils/worker-thread-helper.cjs +154 -27
- package/src/types/native_bindings.ts +1 -1
- package/src/types/options.ts +6 -0
- package/src/windows/drive_status.h +133 -163
- package/src/windows/error_utils.h +54 -3
- package/src/windows/fs_meta.h +1 -1
- package/src/windows/hidden.cpp +60 -43
- package/src/windows/security_utils.h +250 -0
- package/src/windows/string.h +68 -11
- package/src/windows/system_volume.h +1 -1
- package/src/windows/thread_pool.h +206 -0
- package/src/windows/volume_metadata.cpp +11 -6
- package/src/windows/volume_mount_points.cpp +8 -7
- package/src/windows/windows_arch.h +39 -0
- package/scripts/check-memory.mjs +0 -123
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/darwin/path_security.h
|
|
2
|
+
// Secure path validation for macOS using realpath()
|
|
3
|
+
// Implements recommendations from Apple's Secure Coding Guide
|
|
4
|
+
// https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Articles/RaceConditions.html
|
|
5
|
+
|
|
6
|
+
#pragma once
|
|
7
|
+
|
|
8
|
+
#include "../common/debug_log.h"
|
|
9
|
+
#include "../common/error_utils.h"
|
|
10
|
+
#include <cerrno>
|
|
11
|
+
#include <cstring>
|
|
12
|
+
#include <string>
|
|
13
|
+
#include <sys/param.h> // For PATH_MAX
|
|
14
|
+
#include <unistd.h> // For realpath()
|
|
15
|
+
|
|
16
|
+
namespace FSMeta {
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validates a path for security issues and canonicalizes it using realpath().
|
|
20
|
+
*
|
|
21
|
+
* This function prevents directory traversal attacks by:
|
|
22
|
+
* 1. Checking for null bytes (path injection)
|
|
23
|
+
* 2. Using realpath() to resolve symbolic links and path references (../, ./)
|
|
24
|
+
* 3. Handling non-existent paths by validating the parent directory
|
|
25
|
+
*
|
|
26
|
+
* @param path The path to validate
|
|
27
|
+
* @param error Output parameter for error message if validation fails
|
|
28
|
+
* @param allow_nonexistent If true, allows paths that don't exist by validating
|
|
29
|
+
* parent
|
|
30
|
+
* @return The canonicalized path, or empty string if validation fails
|
|
31
|
+
*/
|
|
32
|
+
inline std::string ValidateAndCanonicalizePath(const std::string &path,
|
|
33
|
+
std::string &error,
|
|
34
|
+
bool allow_nonexistent = false) {
|
|
35
|
+
DEBUG_LOG("[ValidateAndCanonicalizePath] Validating path: %s "
|
|
36
|
+
"(allow_nonexistent: %d)",
|
|
37
|
+
path.c_str(), allow_nonexistent);
|
|
38
|
+
|
|
39
|
+
// Check for empty path
|
|
40
|
+
if (path.empty()) {
|
|
41
|
+
error = "Empty path provided";
|
|
42
|
+
DEBUG_LOG("[ValidateAndCanonicalizePath] %s", error.c_str());
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Security check #1: Reject paths with null bytes (path injection attack)
|
|
47
|
+
if (path.find('\0') != std::string::npos) {
|
|
48
|
+
error = "Invalid path containing null byte";
|
|
49
|
+
DEBUG_LOG("[ValidateAndCanonicalizePath] %s", error.c_str());
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Security check #2: Use realpath() to canonicalize and validate
|
|
54
|
+
// realpath() resolves symbolic links and eliminates ../, ./, redundant
|
|
55
|
+
// slashes
|
|
56
|
+
char resolved_path[PATH_MAX];
|
|
57
|
+
if (realpath(path.c_str(), resolved_path) != nullptr) {
|
|
58
|
+
// Path exists and was successfully canonicalized
|
|
59
|
+
std::string canonical_path(resolved_path);
|
|
60
|
+
DEBUG_LOG("[ValidateAndCanonicalizePath] Canonicalized: %s -> %s",
|
|
61
|
+
path.c_str(), canonical_path.c_str());
|
|
62
|
+
return canonical_path;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// realpath() failed - check if it's because the path doesn't exist
|
|
66
|
+
int realpath_error = errno;
|
|
67
|
+
|
|
68
|
+
if (realpath_error == ENOENT && allow_nonexistent) {
|
|
69
|
+
// For operations that create files (like setHidden), validate parent
|
|
70
|
+
// directory
|
|
71
|
+
DEBUG_LOG(
|
|
72
|
+
"[ValidateAndCanonicalizePath] Path doesn't exist, validating parent");
|
|
73
|
+
|
|
74
|
+
// Find the parent directory
|
|
75
|
+
size_t last_slash = path.find_last_of('/');
|
|
76
|
+
std::string parent_dir;
|
|
77
|
+
|
|
78
|
+
if (last_slash == std::string::npos) {
|
|
79
|
+
// No slash found - relative path, use current directory
|
|
80
|
+
parent_dir = ".";
|
|
81
|
+
} else if (last_slash == 0) {
|
|
82
|
+
// Root directory
|
|
83
|
+
parent_dir = "/";
|
|
84
|
+
} else {
|
|
85
|
+
parent_dir = path.substr(0, last_slash);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Validate parent directory exists and is accessible
|
|
89
|
+
if (realpath(parent_dir.c_str(), resolved_path) == nullptr) {
|
|
90
|
+
int parent_error = errno;
|
|
91
|
+
error =
|
|
92
|
+
CreatePathErrorMessage("realpath (parent)", parent_dir, parent_error);
|
|
93
|
+
DEBUG_LOG("[ValidateAndCanonicalizePath] Parent validation failed: %s",
|
|
94
|
+
error.c_str());
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Parent is valid - construct the full path
|
|
99
|
+
std::string parent_canonical(resolved_path);
|
|
100
|
+
std::string filename =
|
|
101
|
+
(last_slash == std::string::npos) ? path : path.substr(last_slash + 1);
|
|
102
|
+
|
|
103
|
+
std::string result;
|
|
104
|
+
if (parent_canonical == "/") {
|
|
105
|
+
result = "/" + filename;
|
|
106
|
+
} else {
|
|
107
|
+
result = parent_canonical + "/" + filename;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
DEBUG_LOG(
|
|
111
|
+
"[ValidateAndCanonicalizePath] Validated non-existent path: %s -> %s",
|
|
112
|
+
path.c_str(), result.c_str());
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// realpath() failed for a different reason, or path doesn't exist and we
|
|
117
|
+
// don't allow it
|
|
118
|
+
error = CreatePathErrorMessage("realpath", path, realpath_error);
|
|
119
|
+
DEBUG_LOG("[ValidateAndCanonicalizePath] Failed: %s", error.c_str());
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Validates that a path is secure for read operations.
|
|
125
|
+
* The path must exist and be accessible.
|
|
126
|
+
*
|
|
127
|
+
* @param path The path to validate
|
|
128
|
+
* @param error Output parameter for error message if validation fails
|
|
129
|
+
* @return The canonicalized path, or empty string if validation fails
|
|
130
|
+
*/
|
|
131
|
+
inline std::string ValidatePathForRead(const std::string &path,
|
|
132
|
+
std::string &error) {
|
|
133
|
+
return ValidateAndCanonicalizePath(path, error, false);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validates that a path is secure for write operations.
|
|
138
|
+
* The path may not exist, but its parent directory must be valid.
|
|
139
|
+
*
|
|
140
|
+
* @param path The path to validate
|
|
141
|
+
* @param error Output parameter for error message if validation fails
|
|
142
|
+
* @return The canonicalized path, or empty string if validation fails
|
|
143
|
+
*/
|
|
144
|
+
inline std::string ValidatePathForWrite(const std::string &path,
|
|
145
|
+
std::string &error) {
|
|
146
|
+
return ValidateAndCanonicalizePath(path, error, true);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
} // namespace FSMeta
|
package/src/darwin/raii_utils.h
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
3
|
#include <CoreFoundation/CoreFoundation.h>
|
|
4
|
+
#include <DiskArbitration/DiskArbitration.h>
|
|
4
5
|
#include <sys/mount.h>
|
|
5
6
|
|
|
7
|
+
// RAII (Resource Acquisition Is Initialization) utilities for macOS APIs.
|
|
8
|
+
// These wrappers ensure proper cleanup of system resources even in the
|
|
9
|
+
// presence of exceptions, preventing memory leaks and resource exhaustion.
|
|
10
|
+
|
|
6
11
|
namespace FSMeta {
|
|
7
12
|
|
|
8
|
-
// Generic RAII wrapper for resources that need free()
|
|
13
|
+
// Generic RAII wrapper for resources that need free().
|
|
14
|
+
// This is used for C-style allocations that must be freed with free().
|
|
15
|
+
// Common usage: buffers returned by system APIs like getmntinfo_r_np().
|
|
9
16
|
template <typename T> class ResourceRAII {
|
|
10
17
|
private:
|
|
11
18
|
T *resource_;
|
|
@@ -41,10 +48,48 @@ public:
|
|
|
41
48
|
ResourceRAII &operator=(const ResourceRAII &) = delete;
|
|
42
49
|
};
|
|
43
50
|
|
|
44
|
-
// Specialized for mount
|
|
45
|
-
|
|
51
|
+
// Specialized RAII wrapper for mount buffer from getmntinfo_r_np().
|
|
52
|
+
// getmntinfo_r_np() allocates a buffer that the caller must free.
|
|
53
|
+
// This wrapper ensures the buffer is freed even if exceptions occur.
|
|
54
|
+
class MountBufferRAII {
|
|
55
|
+
private:
|
|
56
|
+
struct statfs *buffer_;
|
|
57
|
+
|
|
58
|
+
public:
|
|
59
|
+
MountBufferRAII() : buffer_(nullptr) {}
|
|
60
|
+
~MountBufferRAII() {
|
|
61
|
+
if (buffer_) {
|
|
62
|
+
free(buffer_);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
46
65
|
|
|
47
|
-
|
|
66
|
+
struct statfs **ptr() { return &buffer_; }
|
|
67
|
+
struct statfs *get() { return buffer_; }
|
|
68
|
+
|
|
69
|
+
// Add move operations for better resource management
|
|
70
|
+
MountBufferRAII(MountBufferRAII &&other) noexcept : buffer_(other.buffer_) {
|
|
71
|
+
other.buffer_ = nullptr;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
MountBufferRAII &operator=(MountBufferRAII &&other) noexcept {
|
|
75
|
+
if (this != &other) {
|
|
76
|
+
if (buffer_)
|
|
77
|
+
free(buffer_);
|
|
78
|
+
buffer_ = other.buffer_;
|
|
79
|
+
other.buffer_ = nullptr;
|
|
80
|
+
}
|
|
81
|
+
return *this;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Prevent copying
|
|
85
|
+
MountBufferRAII(const MountBufferRAII &) = delete;
|
|
86
|
+
MountBufferRAII &operator=(const MountBufferRAII &) = delete;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// CoreFoundation RAII wrapper following the Create/Copy/Get rule.
|
|
90
|
+
// Any CF object obtained via Create or Copy functions must be released.
|
|
91
|
+
// This wrapper automatically calls CFRelease() in the destructor,
|
|
92
|
+
// preventing memory leaks from Core Foundation objects.
|
|
48
93
|
template <typename T> class CFReleaser {
|
|
49
94
|
private:
|
|
50
95
|
T ref_;
|
|
@@ -82,4 +127,59 @@ public:
|
|
|
82
127
|
}
|
|
83
128
|
};
|
|
84
129
|
|
|
130
|
+
// Specialized RAII wrapper for DASession that handles dispatch queue lifecycle.
|
|
131
|
+
// DASessionSetDispatchQueue must be called with NULL before the session is
|
|
132
|
+
// released. This wrapper ensures proper cleanup order: unschedule then release.
|
|
133
|
+
class DASessionRAII {
|
|
134
|
+
private:
|
|
135
|
+
CFReleaser<DASessionRef> session_;
|
|
136
|
+
bool is_scheduled_;
|
|
137
|
+
|
|
138
|
+
public:
|
|
139
|
+
explicit DASessionRAII(DASessionRef session = nullptr) noexcept
|
|
140
|
+
: session_(session), is_scheduled_(false) {}
|
|
141
|
+
|
|
142
|
+
~DASessionRAII() { unschedule(); }
|
|
143
|
+
|
|
144
|
+
// Schedule the session on a dispatch queue
|
|
145
|
+
void scheduleOnQueue(dispatch_queue_t queue) {
|
|
146
|
+
if (session_.isValid() && queue != nullptr) {
|
|
147
|
+
DASessionSetDispatchQueue(session_.get(), queue);
|
|
148
|
+
is_scheduled_ = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Unschedule the session (must be called before session is released)
|
|
153
|
+
void unschedule() {
|
|
154
|
+
if (is_scheduled_ && session_.isValid()) {
|
|
155
|
+
DASessionSetDispatchQueue(session_.get(), nullptr);
|
|
156
|
+
is_scheduled_ = false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
DASessionRef get() const noexcept { return session_.get(); }
|
|
161
|
+
bool isValid() const noexcept { return session_.isValid(); }
|
|
162
|
+
|
|
163
|
+
// Prevent copying
|
|
164
|
+
DASessionRAII(const DASessionRAII &) = delete;
|
|
165
|
+
DASessionRAII &operator=(const DASessionRAII &) = delete;
|
|
166
|
+
|
|
167
|
+
// Allow moving
|
|
168
|
+
DASessionRAII(DASessionRAII &&other) noexcept
|
|
169
|
+
: session_(std::move(other.session_)),
|
|
170
|
+
is_scheduled_(other.is_scheduled_) {
|
|
171
|
+
other.is_scheduled_ = false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
DASessionRAII &operator=(DASessionRAII &&other) noexcept {
|
|
175
|
+
if (this != &other) {
|
|
176
|
+
unschedule();
|
|
177
|
+
session_ = std::move(other.session_);
|
|
178
|
+
is_scheduled_ = other.is_scheduled_;
|
|
179
|
+
other.is_scheduled_ = false;
|
|
180
|
+
}
|
|
181
|
+
return *this;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
85
185
|
} // namespace FSMeta
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
// src/darwin/volume_metadata.cpp
|
|
2
|
+
// Thread-safe implementation with DiskArbitration mutex synchronization
|
|
2
3
|
|
|
3
4
|
#include "../common/debug_log.h"
|
|
4
5
|
#include "./fs_meta.h"
|
|
6
|
+
#include "./path_security.h"
|
|
5
7
|
#include "./raii_utils.h"
|
|
6
8
|
|
|
7
9
|
#include <CoreFoundation/CoreFoundation.h>
|
|
8
10
|
#include <DiskArbitration/DiskArbitration.h>
|
|
9
|
-
#include <
|
|
10
|
-
#include <IOKit/storage/IOMedia.h>
|
|
11
|
-
#include <IOKit/storage/IOStorageProtocolCharacteristics.h>
|
|
11
|
+
#include <fcntl.h> // For open(), O_RDONLY, O_DIRECTORY
|
|
12
12
|
#include <memory>
|
|
13
|
+
#include <mutex>
|
|
13
14
|
#include <string>
|
|
14
15
|
#include <sys/mount.h>
|
|
15
16
|
#include <sys/param.h>
|
|
16
17
|
#include <sys/statvfs.h>
|
|
18
|
+
#include <unistd.h> // For close()
|
|
17
19
|
|
|
18
20
|
namespace FSMeta {
|
|
19
21
|
|
|
22
|
+
// Global mutex for DiskArbitration operations
|
|
23
|
+
static std::mutex g_diskArbitrationMutex;
|
|
24
|
+
|
|
20
25
|
// Helper function to convert CFString to std::string
|
|
21
26
|
static std::string CFStringToString(CFStringRef cfString) {
|
|
22
27
|
if (!cfString) {
|
|
@@ -24,16 +29,43 @@ static std::string CFStringToString(CFStringRef cfString) {
|
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
CFIndex length = CFStringGetLength(cfString);
|
|
32
|
+
if (length == 0) {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check for overflow when calculating buffer size
|
|
27
37
|
CFIndex maxSize =
|
|
28
|
-
CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8)
|
|
38
|
+
CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8);
|
|
39
|
+
if (maxSize == kCFNotFound || maxSize > INT_MAX - 1) {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
maxSize += 1; // For null terminator
|
|
43
|
+
|
|
29
44
|
std::string result(maxSize, '\0');
|
|
30
45
|
|
|
31
|
-
|
|
32
|
-
|
|
46
|
+
Boolean success =
|
|
47
|
+
CFStringGetCString(cfString, &result[0], maxSize, kCFStringEncodingUTF8);
|
|
48
|
+
if (!success) {
|
|
49
|
+
// Log the failure for debugging
|
|
50
|
+
// Common reasons: encoding issue, buffer too small, or malformed string
|
|
51
|
+
DEBUG_LOG("[CFStringToString] Conversion failed - likely encoding issue or "
|
|
52
|
+
"buffer too small");
|
|
53
|
+
DEBUG_LOG("[CFStringToString] maxSize: %ld, string length: %ld", maxSize,
|
|
54
|
+
CFStringGetLength(cfString));
|
|
33
55
|
return "";
|
|
34
56
|
}
|
|
35
57
|
|
|
36
|
-
|
|
58
|
+
// Find actual string length by looking for null terminator
|
|
59
|
+
// This is safer than using strlen which could read past buffer
|
|
60
|
+
size_t actualLength = 0;
|
|
61
|
+
for (size_t i = 0; i < static_cast<size_t>(maxSize); ++i) {
|
|
62
|
+
if (result[i] == '\0') {
|
|
63
|
+
actualLength = i;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
result.resize(actualLength);
|
|
37
69
|
return result;
|
|
38
70
|
}
|
|
39
71
|
|
|
@@ -48,10 +80,32 @@ public:
|
|
|
48
80
|
DEBUG_LOG("[GetVolumeMetadataWorker] Executing for mount point: %s",
|
|
49
81
|
mountPoint.c_str());
|
|
50
82
|
try {
|
|
83
|
+
// Validate and canonicalize mount point using realpath()
|
|
84
|
+
// This follows Apple's Secure Coding Guide recommendations
|
|
85
|
+
std::string error;
|
|
86
|
+
std::string validated_mount_point =
|
|
87
|
+
ValidatePathForRead(mountPoint, error);
|
|
88
|
+
if (validated_mount_point.empty()) {
|
|
89
|
+
SetError(error);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Use validated path for all subsequent operations
|
|
94
|
+
DEBUG_LOG("[GetVolumeMetadataWorker] Using validated mount point: %s",
|
|
95
|
+
validated_mount_point.c_str());
|
|
96
|
+
|
|
97
|
+
// Temporarily store original mountPoint and replace with validated one
|
|
98
|
+
std::string original_mount_point = mountPoint;
|
|
99
|
+
mountPoint = validated_mount_point;
|
|
100
|
+
|
|
51
101
|
if (!GetBasicVolumeInfo()) {
|
|
102
|
+
mountPoint = original_mount_point; // Restore for error reporting
|
|
52
103
|
return;
|
|
53
104
|
}
|
|
54
|
-
|
|
105
|
+
GetDiskArbitrationInfoSafe();
|
|
106
|
+
|
|
107
|
+
// Restore original for consistency
|
|
108
|
+
mountPoint = original_mount_point;
|
|
55
109
|
} catch (const std::exception &e) {
|
|
56
110
|
DEBUG_LOG("[GetVolumeMetadataWorker] Exception: %s", e.what());
|
|
57
111
|
SetError(e.what());
|
|
@@ -64,23 +118,53 @@ private:
|
|
|
64
118
|
bool GetBasicVolumeInfo() {
|
|
65
119
|
DEBUG_LOG("[GetVolumeMetadataWorker] Getting basic volume info for: %s",
|
|
66
120
|
mountPoint.c_str());
|
|
121
|
+
|
|
122
|
+
// Use file descriptors to prevent TOCTOU race conditions
|
|
123
|
+
// Open the mount point directory with O_DIRECTORY to ensure it's a
|
|
124
|
+
// directory
|
|
125
|
+
int fd = open(mountPoint.c_str(), O_RDONLY | O_DIRECTORY);
|
|
126
|
+
if (fd < 0) {
|
|
127
|
+
int error = errno;
|
|
128
|
+
DEBUG_LOG("[GetVolumeMetadataWorker] open failed: %s (%d)",
|
|
129
|
+
strerror(error), error);
|
|
130
|
+
SetError(CreatePathErrorMessage("open", mountPoint, error));
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// RAII wrapper for file descriptor to ensure it's always closed
|
|
135
|
+
struct FdGuard {
|
|
136
|
+
int fd;
|
|
137
|
+
~FdGuard() {
|
|
138
|
+
if (fd >= 0) {
|
|
139
|
+
close(fd);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} fd_guard{fd};
|
|
143
|
+
|
|
67
144
|
struct statvfs vfs;
|
|
68
145
|
struct statfs fs;
|
|
69
146
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
147
|
+
// Use fstatvfs and fstatfs on the file descriptor to prevent TOCTOU
|
|
148
|
+
// The fd holds a reference to the filesystem, preventing mount changes
|
|
149
|
+
if (fstatvfs(fd, &vfs) != 0) {
|
|
150
|
+
int error = errno;
|
|
151
|
+
DEBUG_LOG("[GetVolumeMetadataWorker] fstatvfs failed: %s (%d)",
|
|
152
|
+
strerror(error), error);
|
|
153
|
+
SetError(CreatePathErrorMessage("fstatvfs", mountPoint, error));
|
|
74
154
|
return false;
|
|
75
155
|
}
|
|
76
156
|
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
157
|
+
if (fstatfs(fd, &fs) != 0) {
|
|
158
|
+
int error = errno;
|
|
159
|
+
DEBUG_LOG("[GetVolumeMetadataWorker] fstatfs failed: %s (%d)",
|
|
160
|
+
strerror(error), error);
|
|
161
|
+
SetError(CreatePathErrorMessage("fstatfs", mountPoint, error));
|
|
81
162
|
return false;
|
|
82
163
|
}
|
|
83
164
|
|
|
165
|
+
// fd_guard will automatically close the file descriptor when this function
|
|
166
|
+
// returns
|
|
167
|
+
|
|
84
168
|
// Calculate sizes using uint64_t to prevent overflow
|
|
85
169
|
const uint64_t blockSize = vfs.f_frsize ? vfs.f_frsize : vfs.f_bsize;
|
|
86
170
|
const uint64_t totalBlocks = static_cast<uint64_t>(vfs.f_blocks);
|
|
@@ -123,7 +207,7 @@ private:
|
|
|
123
207
|
return true;
|
|
124
208
|
}
|
|
125
209
|
|
|
126
|
-
void
|
|
210
|
+
void GetDiskArbitrationInfoSafe() {
|
|
127
211
|
DEBUG_LOG("[GetVolumeMetadataWorker] Getting Disk Arbitration info for: %s",
|
|
128
212
|
mountPoint.c_str());
|
|
129
213
|
|
|
@@ -135,8 +219,18 @@ private:
|
|
|
135
219
|
return;
|
|
136
220
|
}
|
|
137
221
|
|
|
138
|
-
//
|
|
139
|
-
|
|
222
|
+
// THREAD SAFETY NOTE:
|
|
223
|
+
// Apple's DiskArbitration Programming Guide recommends scheduling DASession
|
|
224
|
+
// on a run loop or dispatch queue before using it. We use a dedicated
|
|
225
|
+
// serial dispatch queue (not the main queue) to avoid deadlock in Node.js
|
|
226
|
+
// context while following Apple's documented usage pattern.
|
|
227
|
+
//
|
|
228
|
+
// The mutex serializes DA operations across worker threads for extra
|
|
229
|
+
// safety.
|
|
230
|
+
std::lock_guard<std::mutex> lock(g_diskArbitrationMutex);
|
|
231
|
+
|
|
232
|
+
// Create session with RAII wrapper that handles unscheduling before release
|
|
233
|
+
DASessionRAII session(DASessionCreate(kCFAllocatorDefault));
|
|
140
234
|
if (!session.isValid()) {
|
|
141
235
|
DEBUG_LOG("[GetVolumeMetadataWorker] Failed to create DA session");
|
|
142
236
|
metadata.status = "partial";
|
|
@@ -144,41 +238,18 @@ private:
|
|
|
144
238
|
return;
|
|
145
239
|
}
|
|
146
240
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
throw std::runtime_error("Failed to create thread-local runloop");
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Schedule session with our runloop
|
|
160
|
-
DASessionScheduleWithRunLoop(session.get(), runLoop,
|
|
161
|
-
kCFRunLoopDefaultMode);
|
|
162
|
-
|
|
163
|
-
// Use RAII to ensure cleanup
|
|
164
|
-
struct RunLoopCleaner {
|
|
165
|
-
DASessionRef session;
|
|
166
|
-
CFRunLoopRef runLoop;
|
|
167
|
-
bool shouldStop;
|
|
168
|
-
RunLoopCleaner(DASessionRef s, CFRunLoopRef l, bool stop = false)
|
|
169
|
-
: session(s), runLoop(l), shouldStop(stop) {}
|
|
170
|
-
~RunLoopCleaner() {
|
|
171
|
-
DASessionUnscheduleFromRunLoop(session, runLoop,
|
|
172
|
-
kCFRunLoopDefaultMode);
|
|
173
|
-
if (shouldStop) {
|
|
174
|
-
CFRunLoopStop(runLoop);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
} scopeGuard(session.get(), runLoop, !CFRunLoopGetCurrent());
|
|
241
|
+
// Schedule session on a dedicated serial dispatch queue
|
|
242
|
+
// This follows Apple's documented pattern: create session, schedule it, use
|
|
243
|
+
// it. We use a background queue (not main queue) to avoid deadlock in
|
|
244
|
+
// Node.js. The RAII wrapper will automatically unschedule in its
|
|
245
|
+
// destructor.
|
|
246
|
+
static dispatch_queue_t da_queue =
|
|
247
|
+
dispatch_queue_create("com.photostructure.fs-metadata.diskarbitration",
|
|
248
|
+
DISPATCH_QUEUE_SERIAL);
|
|
178
249
|
|
|
179
|
-
|
|
180
|
-
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, true);
|
|
250
|
+
session.scheduleOnQueue(da_queue);
|
|
181
251
|
|
|
252
|
+
try {
|
|
182
253
|
CFReleaser<DADiskRef> disk(DADiskCreateFromBSDName(
|
|
183
254
|
kCFAllocatorDefault, session.get(), metadata.mountFrom.c_str()));
|
|
184
255
|
|
|
@@ -186,23 +257,22 @@ private:
|
|
|
186
257
|
DEBUG_LOG("[GetVolumeMetadataWorker] Failed to create disk reference");
|
|
187
258
|
metadata.status = "partial";
|
|
188
259
|
metadata.error = "Failed to create disk reference";
|
|
260
|
+
// RAII wrapper will automatically unschedule on function exit
|
|
189
261
|
return;
|
|
190
262
|
}
|
|
191
263
|
|
|
264
|
+
// Now safe to call DADiskCopyDescription with properly scheduled session
|
|
192
265
|
CFReleaser<CFDictionaryRef> description(
|
|
193
266
|
DADiskCopyDescription(disk.get()));
|
|
194
267
|
if (!description.isValid()) {
|
|
195
268
|
DEBUG_LOG("[GetVolumeMetadataWorker] Failed to get disk description");
|
|
196
269
|
metadata.status = "partial";
|
|
197
270
|
metadata.error = "Failed to get disk description";
|
|
271
|
+
// RAII wrapper will automatically unschedule on function exit
|
|
198
272
|
return;
|
|
199
273
|
}
|
|
200
274
|
|
|
201
|
-
//
|
|
202
|
-
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);
|
|
203
|
-
|
|
204
|
-
// Process description synchronously since we're already on the right
|
|
205
|
-
// thread
|
|
275
|
+
// Process description immediately
|
|
206
276
|
ProcessDiskDescription(description.get());
|
|
207
277
|
|
|
208
278
|
if (metadata.status != "partial") {
|
|
@@ -214,6 +284,8 @@ private:
|
|
|
214
284
|
metadata.status = "error";
|
|
215
285
|
metadata.error = e.what();
|
|
216
286
|
}
|
|
287
|
+
|
|
288
|
+
// RAII wrapper automatically unschedules and releases session here
|
|
217
289
|
}
|
|
218
290
|
|
|
219
291
|
void ProcessDiskDescription(CFDictionaryRef description) {
|
|
@@ -248,7 +320,9 @@ private:
|
|
|
248
320
|
DEBUG_LOG("[GetVolumeMetadataWorker] Processing network volume");
|
|
249
321
|
CFBooleanRef isNetworkVolume = (CFBooleanRef)CFDictionaryGetValue(
|
|
250
322
|
description, kDADiskDescriptionVolumeNetworkKey);
|
|
251
|
-
|
|
323
|
+
if (isNetworkVolume) {
|
|
324
|
+
metadata.remote = CFBooleanGetValue(isNetworkVolume);
|
|
325
|
+
}
|
|
252
326
|
|
|
253
327
|
CFURLRef url = (CFURLRef)CFDictionaryGetValue(
|
|
254
328
|
description, kDADiskDescriptionVolumePathKey);
|