@hasna/recordings 0.2.10 → 0.2.13
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/README.md +350 -35
- package/bun.lock +3 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
- package/dist/cli/index.js +1728 -320
- package/dist/cli/macos-permissions.d.ts +13 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -0
- package/dist/cli/options.d.ts +12 -0
- package/dist/cli/options.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +2 -1
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/db/remote-storage.d.ts +5 -1
- package/dist/db/remote-storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +628 -147
- package/dist/lib/bun-runtime.d.ts +11 -0
- package/dist/lib/bun-runtime.d.ts.map +1 -0
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts +10 -2
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/install-maintenance.d.ts +24 -0
- package/dist/lib/install-maintenance.d.ts.map +1 -0
- package/dist/lib/machine.d.ts +2 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +2 -1
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/recording-create-identity.d.ts +13 -0
- package/dist/lib/recording-create-identity.d.ts.map +1 -0
- package/dist/lib/release-install-policy.d.ts +31 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +977 -194
- package/dist/sdk/index.js +5 -1
- package/dist/sdk/v1.generated.d.ts +4 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +9 -0
- package/dist/server/cloud-config.d.ts.map +1 -0
- package/dist/server/cloud-readiness.d.ts +10 -0
- package/dist/server/cloud-readiness.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +9 -18
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1654 -304
- package/dist/server/migrate-command.d.ts +11 -0
- package/dist/server/migrate-command.d.ts.map +1 -0
- package/dist/server/openapi.d.ts +22 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts +8 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +515 -80
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +7 -4
- package/packaging/macos/Empty.entitlements +5 -0
- package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
- package/packaging/macos/Verifier.entitlements +10 -0
- package/packaging/macos/artifact-verifier.sb +33 -0
- package/packaging/macos/build_release_pkg.sh +872 -0
- package/packaging/macos/managed_bootstrap.sh +623 -0
- package/packaging/macos/pkgutil_fingerprint.awk +37 -0
- package/packaging/macos/release_lifecycle.ts +463 -0
- package/packaging/macos/scripts/postinstall +565 -0
- package/packaging/macos/scripts/preinstall +297 -0
- package/scripts/build_companion_cli.sh +337 -0
- package/scripts/build_native_fs_guard.sh +59 -0
- package/scripts/generate-sdk.ts +19 -1
- package/scripts/install_macos_app.sh +1922 -77
- package/scripts/macos_artifact.ts +5316 -0
- package/scripts/migrate.ts +38 -10
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +1167 -0
- package/scripts/native_fs_guard.ts +158 -0
- package/scripts/release-guard.ts +20 -1
- package/scripts/resolve_tailscale_cli.sh +389 -0
- package/scripts/smoke_macos_app.sh +573 -0
- package/src/native/Recordings/App/ContentView.swift +83 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
- package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
- package/src/native/Recordings/App/RecordingsApp.swift +367 -44
- package/src/native/Recordings/App/RecordingsListView.swift +130 -0
- package/src/native/Recordings/App/RecordingsStore.swift +206 -0
- package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
- package/src/native/Recordings/App/SidebarView.swift +212 -0
- package/src/native/Recordings/App/Theme.swift +87 -0
- package/src/native/Recordings/Package.resolved +5 -5
- package/src/native/Recordings/Package.swift +62 -2
- package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
- package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
- package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
- package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
- package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
- package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
- package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
- package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
- package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
- package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
- package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
- package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
- package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
- package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
- package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
- package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
- package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
- package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
- package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
- package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
- package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
- package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
- package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
- package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
- package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
- package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
- package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
- package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
- package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
- package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
- package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
- package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
- package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
- package/src/native/Recordings/build.sh +1990 -29
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
#include "RecordingsVerifierLauncher.h"
|
|
2
|
+
|
|
3
|
+
#include <errno.h>
|
|
4
|
+
#include <dirent.h>
|
|
5
|
+
#include <fcntl.h>
|
|
6
|
+
#include <grp.h>
|
|
7
|
+
#include <limits.h>
|
|
8
|
+
#include <pwd.h>
|
|
9
|
+
#include <sandbox.h>
|
|
10
|
+
#include <signal.h>
|
|
11
|
+
#include <stdbool.h>
|
|
12
|
+
#include <stdint.h>
|
|
13
|
+
#include <stdio.h>
|
|
14
|
+
#include <stdlib.h>
|
|
15
|
+
#include <string.h>
|
|
16
|
+
#include <sys/acl.h>
|
|
17
|
+
#include <sys/resource.h>
|
|
18
|
+
#include <sys/stat.h>
|
|
19
|
+
#include <sys/types.h>
|
|
20
|
+
#include <sys/wait.h>
|
|
21
|
+
#include <time.h>
|
|
22
|
+
#include <unistd.h>
|
|
23
|
+
|
|
24
|
+
#define VERIFIER_PATH "/Library/PrivilegedHelperTools/com.hasna.recordings.artifact-verifier"
|
|
25
|
+
#define PROFILE_PATH "/Library/Application Support/Hasna/Recordings/Trust/artifact-verifier.sb"
|
|
26
|
+
#define MAX_PROFILE_BYTES (64 * 1024)
|
|
27
|
+
#define CHILD_ARCHIVE_FD 3
|
|
28
|
+
#define CHILD_OUTPUT_FD 4
|
|
29
|
+
#define OUTPUT_PATH_PREFIX "/Library/Application Support/Hasna/Recordings/Updates/transaction-"
|
|
30
|
+
#define OUTPUT_PATH_SUFFIX "/verifier-output"
|
|
31
|
+
|
|
32
|
+
int recordings_descriptor_has_no_extended_acl(int descriptor) {
|
|
33
|
+
if (descriptor < 0) return 0;
|
|
34
|
+
errno = 0;
|
|
35
|
+
acl_t acl = acl_get_fd_np(descriptor, ACL_TYPE_EXTENDED);
|
|
36
|
+
if (acl == NULL) return errno == ENOENT ? 1 : 0;
|
|
37
|
+
(void)acl_free(acl);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static int validate_root_owned_directory_path(const char *path) {
|
|
42
|
+
int descriptor = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
|
|
43
|
+
if (descriptor < 0) return errno;
|
|
44
|
+
struct stat metadata;
|
|
45
|
+
const bool valid = fstat(descriptor, &metadata) == 0 &&
|
|
46
|
+
S_ISDIR(metadata.st_mode) && metadata.st_uid == 0 &&
|
|
47
|
+
(metadata.st_mode & 0022) == 0 &&
|
|
48
|
+
recordings_descriptor_has_no_extended_acl(descriptor);
|
|
49
|
+
close(descriptor);
|
|
50
|
+
return valid ? 0 : EPERM;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static int validate_fixed_runtime_ancestry(void) {
|
|
54
|
+
const char *directories[] = {
|
|
55
|
+
"/",
|
|
56
|
+
"/Library",
|
|
57
|
+
"/Library/PrivilegedHelperTools",
|
|
58
|
+
"/Library/Application Support",
|
|
59
|
+
"/Library/Application Support/Hasna",
|
|
60
|
+
"/Library/Application Support/Hasna/Recordings",
|
|
61
|
+
"/Library/Application Support/Hasna/Recordings/Trust",
|
|
62
|
+
};
|
|
63
|
+
for (size_t index = 0; index < sizeof(directories) / sizeof(directories[0]); index += 1) {
|
|
64
|
+
const int status = validate_root_owned_directory_path(directories[index]);
|
|
65
|
+
if (status != 0) return status;
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static bool ends_with(const char *value, const char *suffix) {
|
|
71
|
+
const size_t value_length = strlen(value);
|
|
72
|
+
const size_t suffix_length = strlen(suffix);
|
|
73
|
+
return value_length >= suffix_length &&
|
|
74
|
+
strcmp(value + value_length - suffix_length, suffix) == 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
int recordings_lookup_verifier_account(
|
|
78
|
+
const char *account_name,
|
|
79
|
+
uid_t *user_id,
|
|
80
|
+
gid_t *group_id
|
|
81
|
+
) {
|
|
82
|
+
if (account_name == NULL || user_id == NULL || group_id == NULL) return EINVAL;
|
|
83
|
+
long suggested = sysconf(_SC_GETPW_R_SIZE_MAX);
|
|
84
|
+
if (suggested < 4096) suggested = 4096;
|
|
85
|
+
if (suggested > 1024 * 1024) return EOVERFLOW;
|
|
86
|
+
char *buffer = calloc(1, (size_t)suggested);
|
|
87
|
+
if (buffer == NULL) return ENOMEM;
|
|
88
|
+
struct passwd record;
|
|
89
|
+
struct passwd *result = NULL;
|
|
90
|
+
const int status = getpwnam_r(account_name, &record, buffer, (size_t)suggested, &result);
|
|
91
|
+
if (status != 0 || result == NULL || record.pw_uid == 0 || record.pw_gid == 0 ||
|
|
92
|
+
record.pw_dir == NULL || strcmp(record.pw_dir, "/var/empty") != 0 ||
|
|
93
|
+
record.pw_shell == NULL ||
|
|
94
|
+
(!ends_with(record.pw_shell, "/false") && !ends_with(record.pw_shell, "/nologin"))) {
|
|
95
|
+
free(buffer);
|
|
96
|
+
return status != 0 ? status : EPERM;
|
|
97
|
+
}
|
|
98
|
+
*user_id = record.pw_uid;
|
|
99
|
+
*group_id = record.pw_gid;
|
|
100
|
+
free(buffer);
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static int open_and_read_profile(char **profile_out) {
|
|
105
|
+
int descriptor = open(PROFILE_PATH, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
|
|
106
|
+
if (descriptor < 0) return errno;
|
|
107
|
+
struct stat metadata;
|
|
108
|
+
if (fstat(descriptor, &metadata) != 0 || !S_ISREG(metadata.st_mode) ||
|
|
109
|
+
metadata.st_uid != 0 || (metadata.st_mode & 0022) != 0 ||
|
|
110
|
+
metadata.st_size <= 0 || metadata.st_size > MAX_PROFILE_BYTES ||
|
|
111
|
+
!recordings_descriptor_has_no_extended_acl(descriptor)) {
|
|
112
|
+
close(descriptor);
|
|
113
|
+
return EPERM;
|
|
114
|
+
}
|
|
115
|
+
char *profile = calloc(1, (size_t)metadata.st_size + 1);
|
|
116
|
+
if (profile == NULL) {
|
|
117
|
+
close(descriptor);
|
|
118
|
+
return ENOMEM;
|
|
119
|
+
}
|
|
120
|
+
off_t offset = 0;
|
|
121
|
+
while (offset < metadata.st_size) {
|
|
122
|
+
const ssize_t count = pread(
|
|
123
|
+
descriptor,
|
|
124
|
+
profile + offset,
|
|
125
|
+
(size_t)(metadata.st_size - offset),
|
|
126
|
+
offset
|
|
127
|
+
);
|
|
128
|
+
if (count < 0 && errno == EINTR) continue;
|
|
129
|
+
if (count <= 0) {
|
|
130
|
+
free(profile);
|
|
131
|
+
close(descriptor);
|
|
132
|
+
return EIO;
|
|
133
|
+
}
|
|
134
|
+
offset += count;
|
|
135
|
+
}
|
|
136
|
+
struct stat after;
|
|
137
|
+
if (fstat(descriptor, &after) != 0 || metadata.st_dev != after.st_dev ||
|
|
138
|
+
metadata.st_ino != after.st_ino || metadata.st_size != after.st_size ||
|
|
139
|
+
metadata.st_mtimespec.tv_sec != after.st_mtimespec.tv_sec ||
|
|
140
|
+
metadata.st_mtimespec.tv_nsec != after.st_mtimespec.tv_nsec ||
|
|
141
|
+
metadata.st_ctimespec.tv_sec != after.st_ctimespec.tv_sec ||
|
|
142
|
+
metadata.st_ctimespec.tv_nsec != after.st_ctimespec.tv_nsec ||
|
|
143
|
+
!recordings_descriptor_has_no_extended_acl(descriptor)) {
|
|
144
|
+
free(profile);
|
|
145
|
+
close(descriptor);
|
|
146
|
+
return EBUSY;
|
|
147
|
+
}
|
|
148
|
+
close(descriptor);
|
|
149
|
+
*profile_out = profile;
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static int verify_protected_executable(void) {
|
|
154
|
+
int descriptor = open(VERIFIER_PATH, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
|
|
155
|
+
if (descriptor < 0) return errno;
|
|
156
|
+
struct stat metadata;
|
|
157
|
+
const bool valid = fstat(descriptor, &metadata) == 0 &&
|
|
158
|
+
S_ISREG(metadata.st_mode) && metadata.st_uid == 0 &&
|
|
159
|
+
(metadata.st_mode & 0022) == 0 && metadata.st_nlink == 1 &&
|
|
160
|
+
recordings_descriptor_has_no_extended_acl(descriptor);
|
|
161
|
+
close(descriptor);
|
|
162
|
+
return valid ? 0 : EPERM;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
static int duplicate_at_least(int descriptor, int minimum) {
|
|
166
|
+
int result;
|
|
167
|
+
do {
|
|
168
|
+
result = fcntl(descriptor, F_DUPFD_CLOEXEC, minimum);
|
|
169
|
+
} while (result < 0 && errno == EINTR);
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static bool is_lower_hex_character(char value) {
|
|
174
|
+
return (value >= '0' && value <= '9') || (value >= 'a' && value <= 'f');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static bool is_valid_expected_sha256(const char *value) {
|
|
178
|
+
if (value == NULL || strlen(value) != 64) return false;
|
|
179
|
+
for (size_t index = 0; index < 64; index += 1) {
|
|
180
|
+
if (!is_lower_hex_character(value[index])) return false;
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
static bool is_valid_transaction_name(const char *name) {
|
|
186
|
+
const char *prefix = "transaction-";
|
|
187
|
+
const size_t prefix_length = strlen(prefix);
|
|
188
|
+
if (name == NULL || strlen(name) != prefix_length + 36 ||
|
|
189
|
+
strncmp(name, prefix, prefix_length) != 0) return false;
|
|
190
|
+
const char *identifier = name + prefix_length;
|
|
191
|
+
for (size_t index = 0; index < 36; index += 1) {
|
|
192
|
+
const bool hyphen = index == 8 || index == 13 || index == 18 || index == 23;
|
|
193
|
+
if ((hyphen && identifier[index] != '-') ||
|
|
194
|
+
(!hyphen && !is_lower_hex_character(identifier[index]))) return false;
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
static bool is_valid_transaction_output_path(const char *path) {
|
|
200
|
+
const size_t prefix_length = strlen(OUTPUT_PATH_PREFIX);
|
|
201
|
+
if (strncmp(path, OUTPUT_PATH_PREFIX, prefix_length) != 0) return false;
|
|
202
|
+
const char *identifier = path + prefix_length;
|
|
203
|
+
for (size_t index = 0; index < 36; index += 1) {
|
|
204
|
+
const bool hyphen = index == 8 || index == 13 || index == 18 || index == 23;
|
|
205
|
+
if ((hyphen && identifier[index] != '-') ||
|
|
206
|
+
(!hyphen && !is_lower_hex_character(identifier[index]))) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return strcmp(identifier + 36, OUTPUT_PATH_SUFFIX) == 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
static int validate_inherited_descriptors(
|
|
214
|
+
int archive_descriptor,
|
|
215
|
+
int output_directory_descriptor,
|
|
216
|
+
uid_t verifier_user_id,
|
|
217
|
+
gid_t verifier_group_id,
|
|
218
|
+
char output_path[PATH_MAX]
|
|
219
|
+
) {
|
|
220
|
+
struct stat archive;
|
|
221
|
+
if (fstat(archive_descriptor, &archive) != 0 || !S_ISREG(archive.st_mode) ||
|
|
222
|
+
archive.st_uid != 0 || (archive.st_mode & 0077) != 0 || archive.st_nlink != 1 ||
|
|
223
|
+
archive.st_size <= 0 ||
|
|
224
|
+
!recordings_descriptor_has_no_extended_acl(archive_descriptor)) {
|
|
225
|
+
return EPERM;
|
|
226
|
+
}
|
|
227
|
+
struct stat output;
|
|
228
|
+
if (fstat(output_directory_descriptor, &output) != 0 || !S_ISDIR(output.st_mode) ||
|
|
229
|
+
output.st_uid != verifier_user_id || output.st_gid != verifier_group_id ||
|
|
230
|
+
(output.st_mode & 0077) != 0 ||
|
|
231
|
+
!recordings_descriptor_has_no_extended_acl(output_directory_descriptor)) {
|
|
232
|
+
return EPERM;
|
|
233
|
+
}
|
|
234
|
+
memset(output_path, 0, PATH_MAX);
|
|
235
|
+
if (fcntl(output_directory_descriptor, F_GETPATH, output_path) != 0 ||
|
|
236
|
+
output_path[0] != '/' || !is_valid_transaction_output_path(output_path)) {
|
|
237
|
+
return EPERM;
|
|
238
|
+
}
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
static bool apply_limit(int resource, rlim_t soft, rlim_t hard) {
|
|
243
|
+
const struct rlimit limit = { .rlim_cur = soft, .rlim_max = hard };
|
|
244
|
+
return setrlimit(resource, &limit) == 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
static void kill_and_reap(pid_t child) {
|
|
248
|
+
kill(child, SIGKILL);
|
|
249
|
+
int wait_status = 0;
|
|
250
|
+
while (waitpid(child, &wait_status, 0) < 0 && errno == EINTR) {}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
static void verifier_child(
|
|
254
|
+
int archive_descriptor,
|
|
255
|
+
int output_descriptor,
|
|
256
|
+
uid_t user_id,
|
|
257
|
+
gid_t group_id,
|
|
258
|
+
const char *expected_sha256,
|
|
259
|
+
const char *sandbox_profile,
|
|
260
|
+
const char *output_path
|
|
261
|
+
) {
|
|
262
|
+
if (!apply_limit(RLIMIT_CPU, 60, 60) ||
|
|
263
|
+
!apply_limit(RLIMIT_AS, 4ULL * 1024 * 1024 * 1024, 4ULL * 1024 * 1024 * 1024) ||
|
|
264
|
+
!apply_limit(RLIMIT_FSIZE, 4ULL * 1024 * 1024 * 1024, 4ULL * 1024 * 1024 * 1024) ||
|
|
265
|
+
!apply_limit(RLIMIT_NOFILE, 32, 32) ||
|
|
266
|
+
!apply_limit(RLIMIT_NPROC, 1, 1)) {
|
|
267
|
+
_exit(70);
|
|
268
|
+
}
|
|
269
|
+
if (dup2(archive_descriptor, CHILD_ARCHIVE_FD) < 0 ||
|
|
270
|
+
dup2(output_descriptor, CHILD_OUTPUT_FD) < 0) {
|
|
271
|
+
_exit(73);
|
|
272
|
+
}
|
|
273
|
+
int null_descriptor = open("/dev/null", O_RDWR | O_CLOEXEC | O_NOFOLLOW);
|
|
274
|
+
if (null_descriptor < 0 || dup2(null_descriptor, STDIN_FILENO) < 0 ||
|
|
275
|
+
dup2(null_descriptor, STDOUT_FILENO) < 0 || dup2(null_descriptor, STDERR_FILENO) < 0) {
|
|
276
|
+
_exit(74);
|
|
277
|
+
}
|
|
278
|
+
if (null_descriptor > CHILD_OUTPUT_FD) close(null_descriptor);
|
|
279
|
+
closefrom(5);
|
|
280
|
+
|
|
281
|
+
gid_t groups[] = { group_id };
|
|
282
|
+
if (setgroups(1, groups) != 0 || setgid(group_id) != 0 || setuid(user_id) != 0 ||
|
|
283
|
+
geteuid() != user_id || getegid() != group_id) {
|
|
284
|
+
_exit(71);
|
|
285
|
+
}
|
|
286
|
+
const char *const sandbox_parameters[] = {
|
|
287
|
+
"OUTPUT_DIR", output_path,
|
|
288
|
+
NULL,
|
|
289
|
+
};
|
|
290
|
+
char *sandbox_error = NULL;
|
|
291
|
+
if (sandbox_init_with_parameters(sandbox_profile, 0, sandbox_parameters, &sandbox_error) != 0) {
|
|
292
|
+
if (sandbox_error != NULL) sandbox_free_error(sandbox_error);
|
|
293
|
+
_exit(72);
|
|
294
|
+
}
|
|
295
|
+
// Sandbox initialization is allowed to use implementation-internal descriptors.
|
|
296
|
+
// Re-close everything above the two explicit artifact descriptors before exec.
|
|
297
|
+
closefrom(5);
|
|
298
|
+
|
|
299
|
+
char archive_argument[16];
|
|
300
|
+
char output_argument[16];
|
|
301
|
+
snprintf(archive_argument, sizeof(archive_argument), "%d", CHILD_ARCHIVE_FD);
|
|
302
|
+
snprintf(output_argument, sizeof(output_argument), "%d", CHILD_OUTPUT_FD);
|
|
303
|
+
char *const arguments[] = {
|
|
304
|
+
(char *)VERIFIER_PATH,
|
|
305
|
+
(char *)"verify",
|
|
306
|
+
(char *)"--archive-fd",
|
|
307
|
+
archive_argument,
|
|
308
|
+
(char *)"--output-dir-fd",
|
|
309
|
+
output_argument,
|
|
310
|
+
(char *)"--expected-sha256",
|
|
311
|
+
(char *)expected_sha256,
|
|
312
|
+
NULL,
|
|
313
|
+
};
|
|
314
|
+
char *const environment[] = { NULL };
|
|
315
|
+
execve(VERIFIER_PATH, arguments, environment);
|
|
316
|
+
_exit(75);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
int recordings_run_artifact_verifier(
|
|
320
|
+
int archive_descriptor,
|
|
321
|
+
int output_directory_descriptor,
|
|
322
|
+
uid_t verifier_user_id,
|
|
323
|
+
gid_t verifier_group_id,
|
|
324
|
+
const char *expected_archive_sha256
|
|
325
|
+
) {
|
|
326
|
+
if (archive_descriptor < 0 || output_directory_descriptor < 0 ||
|
|
327
|
+
verifier_user_id == 0 || verifier_group_id == 0 ||
|
|
328
|
+
!is_valid_expected_sha256(expected_archive_sha256)) {
|
|
329
|
+
return EINVAL;
|
|
330
|
+
}
|
|
331
|
+
int status = validate_fixed_runtime_ancestry();
|
|
332
|
+
if (status != 0) return status;
|
|
333
|
+
char output_path[PATH_MAX];
|
|
334
|
+
status = validate_inherited_descriptors(
|
|
335
|
+
archive_descriptor,
|
|
336
|
+
output_directory_descriptor,
|
|
337
|
+
verifier_user_id,
|
|
338
|
+
verifier_group_id,
|
|
339
|
+
output_path
|
|
340
|
+
);
|
|
341
|
+
if (status != 0) return status;
|
|
342
|
+
char *sandbox_profile = NULL;
|
|
343
|
+
status = open_and_read_profile(&sandbox_profile);
|
|
344
|
+
if (status != 0) return status;
|
|
345
|
+
status = verify_protected_executable();
|
|
346
|
+
if (status != 0) {
|
|
347
|
+
free(sandbox_profile);
|
|
348
|
+
return status;
|
|
349
|
+
}
|
|
350
|
+
int archive_copy = duplicate_at_least(archive_descriptor, 10);
|
|
351
|
+
int output_copy = duplicate_at_least(output_directory_descriptor, 10);
|
|
352
|
+
if (archive_copy < 0 || output_copy < 0) {
|
|
353
|
+
if (archive_copy >= 0) close(archive_copy);
|
|
354
|
+
if (output_copy >= 0) close(output_copy);
|
|
355
|
+
free(sandbox_profile);
|
|
356
|
+
return errno != 0 ? errno : EMFILE;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const pid_t child = fork();
|
|
360
|
+
if (child < 0) {
|
|
361
|
+
status = errno;
|
|
362
|
+
} else if (child == 0) {
|
|
363
|
+
verifier_child(
|
|
364
|
+
archive_copy,
|
|
365
|
+
output_copy,
|
|
366
|
+
verifier_user_id,
|
|
367
|
+
verifier_group_id,
|
|
368
|
+
expected_archive_sha256,
|
|
369
|
+
sandbox_profile,
|
|
370
|
+
output_path
|
|
371
|
+
);
|
|
372
|
+
} else {
|
|
373
|
+
struct timespec started;
|
|
374
|
+
if (clock_gettime(CLOCK_MONOTONIC, &started) != 0) {
|
|
375
|
+
kill_and_reap(child);
|
|
376
|
+
status = errno != 0 ? errno : EIO;
|
|
377
|
+
}
|
|
378
|
+
int wait_status = 0;
|
|
379
|
+
while (status == 0) {
|
|
380
|
+
const pid_t waited = waitpid(child, &wait_status, WNOHANG);
|
|
381
|
+
if (waited == child) {
|
|
382
|
+
status = WIFEXITED(wait_status) && WEXITSTATUS(wait_status) == 0 ? 0 : EPROTO;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
if (waited < 0 && errno != EINTR) {
|
|
386
|
+
const int wait_error = errno;
|
|
387
|
+
kill_and_reap(child);
|
|
388
|
+
status = wait_error != 0 ? wait_error : ECHILD;
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
struct timespec now;
|
|
392
|
+
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
|
|
393
|
+
const int clock_error = errno;
|
|
394
|
+
kill_and_reap(child);
|
|
395
|
+
status = clock_error != 0 ? clock_error : EIO;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
if (now.tv_sec - started.tv_sec >= 90) {
|
|
399
|
+
kill_and_reap(child);
|
|
400
|
+
status = ETIMEDOUT;
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
const struct timespec pause = { .tv_sec = 0, .tv_nsec = 100000000 };
|
|
404
|
+
while (nanosleep(&pause, NULL) != 0 && errno == EINTR) {}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
close(archive_copy);
|
|
408
|
+
close(output_copy);
|
|
409
|
+
free(sandbox_profile);
|
|
410
|
+
return status;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
struct copy_budget {
|
|
414
|
+
uint64_t entries;
|
|
415
|
+
uint64_t bytes;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
static bool same_stable_binding(const struct stat *before, const struct stat *after) {
|
|
419
|
+
return before->st_dev == after->st_dev && before->st_ino == after->st_ino &&
|
|
420
|
+
before->st_size == after->st_size &&
|
|
421
|
+
before->st_mtimespec.tv_sec == after->st_mtimespec.tv_sec &&
|
|
422
|
+
before->st_mtimespec.tv_nsec == after->st_mtimespec.tv_nsec &&
|
|
423
|
+
before->st_ctimespec.tv_sec == after->st_ctimespec.tv_sec &&
|
|
424
|
+
before->st_ctimespec.tv_nsec == after->st_ctimespec.tv_nsec;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
static int copy_regular_at(
|
|
428
|
+
int source_parent,
|
|
429
|
+
int destination_parent,
|
|
430
|
+
const char *name,
|
|
431
|
+
uid_t verifier_user_id,
|
|
432
|
+
struct copy_budget *budget
|
|
433
|
+
) {
|
|
434
|
+
struct stat named;
|
|
435
|
+
if (fstatat(source_parent, name, &named, AT_SYMLINK_NOFOLLOW) != 0 ||
|
|
436
|
+
!S_ISREG(named.st_mode) || named.st_uid != verifier_user_id ||
|
|
437
|
+
named.st_nlink != 1 || (named.st_mode & 0022) != 0 ||
|
|
438
|
+
(named.st_mode & (S_ISUID | S_ISGID | S_ISVTX)) != 0 || named.st_size < 0) {
|
|
439
|
+
return EPERM;
|
|
440
|
+
}
|
|
441
|
+
if (budget->entries >= 100000 ||
|
|
442
|
+
(uint64_t)named.st_size > (4ULL * 1024 * 1024 * 1024) - budget->bytes) {
|
|
443
|
+
return EFBIG;
|
|
444
|
+
}
|
|
445
|
+
budget->entries += 1;
|
|
446
|
+
budget->bytes += (uint64_t)named.st_size;
|
|
447
|
+
int input = openat(source_parent, name, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
|
|
448
|
+
if (input < 0) return errno;
|
|
449
|
+
struct stat opened;
|
|
450
|
+
if (fstat(input, &opened) != 0 || named.st_dev != opened.st_dev ||
|
|
451
|
+
named.st_ino != opened.st_ino || named.st_size != opened.st_size ||
|
|
452
|
+
!recordings_descriptor_has_no_extended_acl(input)) {
|
|
453
|
+
close(input);
|
|
454
|
+
return EBUSY;
|
|
455
|
+
}
|
|
456
|
+
int output = openat(
|
|
457
|
+
destination_parent,
|
|
458
|
+
name,
|
|
459
|
+
O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW,
|
|
460
|
+
0600
|
|
461
|
+
);
|
|
462
|
+
if (output < 0) {
|
|
463
|
+
int status = errno;
|
|
464
|
+
close(input);
|
|
465
|
+
return status;
|
|
466
|
+
}
|
|
467
|
+
if (!recordings_descriptor_has_no_extended_acl(output)) {
|
|
468
|
+
close(input);
|
|
469
|
+
close(output);
|
|
470
|
+
unlinkat(destination_parent, name, 0);
|
|
471
|
+
return EPERM;
|
|
472
|
+
}
|
|
473
|
+
int status = 0;
|
|
474
|
+
uint8_t *buffer = malloc(1024 * 1024);
|
|
475
|
+
if (buffer == NULL) status = ENOMEM;
|
|
476
|
+
off_t offset = 0;
|
|
477
|
+
while (status == 0 && offset < opened.st_size) {
|
|
478
|
+
size_t requested = (size_t)(opened.st_size - offset);
|
|
479
|
+
if (requested > 1024 * 1024) requested = 1024 * 1024;
|
|
480
|
+
ssize_t count = pread(input, buffer, requested, offset);
|
|
481
|
+
if (count < 0 && errno == EINTR) continue;
|
|
482
|
+
if (count <= 0) { status = EIO; break; }
|
|
483
|
+
ssize_t written = 0;
|
|
484
|
+
while (written < count) {
|
|
485
|
+
ssize_t chunk = write(output, buffer + written, (size_t)(count - written));
|
|
486
|
+
if (chunk < 0 && errno == EINTR) continue;
|
|
487
|
+
if (chunk <= 0) { status = EIO; break; }
|
|
488
|
+
written += chunk;
|
|
489
|
+
}
|
|
490
|
+
offset += count;
|
|
491
|
+
}
|
|
492
|
+
free(buffer);
|
|
493
|
+
struct stat after;
|
|
494
|
+
if (status == 0 &&
|
|
495
|
+
(fstat(input, &after) != 0 || !same_stable_binding(&opened, &after) ||
|
|
496
|
+
!recordings_descriptor_has_no_extended_acl(input))) {
|
|
497
|
+
status = EBUSY;
|
|
498
|
+
}
|
|
499
|
+
if (status == 0 && fchmod(output, opened.st_mode & 0755) != 0) status = errno;
|
|
500
|
+
if (status == 0 && !recordings_descriptor_has_no_extended_acl(output)) status = EPERM;
|
|
501
|
+
if (status == 0 && fsync(output) != 0) status = errno != 0 ? errno : EIO;
|
|
502
|
+
close(input);
|
|
503
|
+
close(output);
|
|
504
|
+
if (status != 0) unlinkat(destination_parent, name, 0);
|
|
505
|
+
return status;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
static int copy_directory_at(
|
|
509
|
+
int source_parent,
|
|
510
|
+
int destination_parent,
|
|
511
|
+
const char *name,
|
|
512
|
+
uid_t verifier_user_id,
|
|
513
|
+
struct copy_budget *budget
|
|
514
|
+
) {
|
|
515
|
+
struct stat named;
|
|
516
|
+
if (fstatat(source_parent, name, &named, AT_SYMLINK_NOFOLLOW) != 0 ||
|
|
517
|
+
!S_ISDIR(named.st_mode) || named.st_uid != verifier_user_id ||
|
|
518
|
+
(named.st_mode & 0022) != 0 ||
|
|
519
|
+
(named.st_mode & (S_ISUID | S_ISGID | S_ISVTX)) != 0) {
|
|
520
|
+
return EPERM;
|
|
521
|
+
}
|
|
522
|
+
if (budget->entries >= 100000) return EFBIG;
|
|
523
|
+
budget->entries += 1;
|
|
524
|
+
int source = openat(source_parent, name, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
|
|
525
|
+
if (source < 0) return errno;
|
|
526
|
+
struct stat opened;
|
|
527
|
+
if (fstat(source, &opened) != 0 || named.st_dev != opened.st_dev ||
|
|
528
|
+
named.st_ino != opened.st_ino ||
|
|
529
|
+
!recordings_descriptor_has_no_extended_acl(source)) {
|
|
530
|
+
close(source);
|
|
531
|
+
return EBUSY;
|
|
532
|
+
}
|
|
533
|
+
if (mkdirat(destination_parent, name, 0700) != 0) {
|
|
534
|
+
int status = errno;
|
|
535
|
+
close(source);
|
|
536
|
+
return status;
|
|
537
|
+
}
|
|
538
|
+
int destination = openat(
|
|
539
|
+
destination_parent,
|
|
540
|
+
name,
|
|
541
|
+
O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW
|
|
542
|
+
);
|
|
543
|
+
if (destination < 0) {
|
|
544
|
+
int status = errno;
|
|
545
|
+
close(source);
|
|
546
|
+
return status;
|
|
547
|
+
}
|
|
548
|
+
if (!recordings_descriptor_has_no_extended_acl(destination)) {
|
|
549
|
+
close(source);
|
|
550
|
+
close(destination);
|
|
551
|
+
unlinkat(destination_parent, name, AT_REMOVEDIR);
|
|
552
|
+
return EPERM;
|
|
553
|
+
}
|
|
554
|
+
int enumeration_descriptor = duplicate_at_least(source, 10);
|
|
555
|
+
DIR *directory = enumeration_descriptor < 0 ? NULL : fdopendir(enumeration_descriptor);
|
|
556
|
+
int status = directory == NULL ? (errno != 0 ? errno : EIO) : 0;
|
|
557
|
+
if (directory == NULL && enumeration_descriptor >= 0) close(enumeration_descriptor);
|
|
558
|
+
while (status == 0) {
|
|
559
|
+
errno = 0;
|
|
560
|
+
struct dirent *entry = readdir(directory);
|
|
561
|
+
if (entry == NULL) {
|
|
562
|
+
if (errno != 0) status = errno;
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
|
|
566
|
+
if (strchr(entry->d_name, '/') != NULL || entry->d_name[0] == '\0') {
|
|
567
|
+
status = EPERM;
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
struct stat child;
|
|
571
|
+
if (fstatat(source, entry->d_name, &child, AT_SYMLINK_NOFOLLOW) != 0) {
|
|
572
|
+
status = errno;
|
|
573
|
+
} else if (S_ISDIR(child.st_mode)) {
|
|
574
|
+
status = copy_directory_at(source, destination, entry->d_name, verifier_user_id, budget);
|
|
575
|
+
} else if (S_ISREG(child.st_mode)) {
|
|
576
|
+
status = copy_regular_at(source, destination, entry->d_name, verifier_user_id, budget);
|
|
577
|
+
} else {
|
|
578
|
+
status = EPERM;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (directory != NULL) closedir(directory);
|
|
582
|
+
struct stat after;
|
|
583
|
+
if (status == 0 &&
|
|
584
|
+
(fstat(source, &after) != 0 || !same_stable_binding(&opened, &after) ||
|
|
585
|
+
!recordings_descriptor_has_no_extended_acl(source))) {
|
|
586
|
+
status = EBUSY;
|
|
587
|
+
}
|
|
588
|
+
if (status == 0 && fchmod(destination, opened.st_mode & 0755) != 0) status = errno;
|
|
589
|
+
if (status == 0 && !recordings_descriptor_has_no_extended_acl(destination)) status = EPERM;
|
|
590
|
+
if (status == 0 && fsync(destination) != 0) status = errno != 0 ? errno : EIO;
|
|
591
|
+
close(source);
|
|
592
|
+
close(destination);
|
|
593
|
+
return status;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
int recordings_copy_canonical_application_tree(
|
|
597
|
+
int verifier_output_directory_descriptor,
|
|
598
|
+
int root_candidate_directory_descriptor,
|
|
599
|
+
uid_t verifier_user_id
|
|
600
|
+
) {
|
|
601
|
+
if (verifier_output_directory_descriptor < 0 || root_candidate_directory_descriptor < 0 ||
|
|
602
|
+
verifier_user_id == 0) return EINVAL;
|
|
603
|
+
struct stat source_root;
|
|
604
|
+
struct stat destination_root;
|
|
605
|
+
if (fstat(verifier_output_directory_descriptor, &source_root) != 0 ||
|
|
606
|
+
!S_ISDIR(source_root.st_mode) || source_root.st_uid != 0 ||
|
|
607
|
+
(source_root.st_mode & 0077) != 0 ||
|
|
608
|
+
fstat(root_candidate_directory_descriptor, &destination_root) != 0 ||
|
|
609
|
+
!S_ISDIR(destination_root.st_mode) || destination_root.st_uid != 0 ||
|
|
610
|
+
(destination_root.st_mode & 0077) != 0 ||
|
|
611
|
+
!recordings_descriptor_has_no_extended_acl(verifier_output_directory_descriptor) ||
|
|
612
|
+
!recordings_descriptor_has_no_extended_acl(root_candidate_directory_descriptor)) {
|
|
613
|
+
return EPERM;
|
|
614
|
+
}
|
|
615
|
+
int enumeration_descriptor = duplicate_at_least(verifier_output_directory_descriptor, 10);
|
|
616
|
+
if (enumeration_descriptor < 0) return errno;
|
|
617
|
+
DIR *directory = fdopendir(enumeration_descriptor);
|
|
618
|
+
if (directory == NULL) {
|
|
619
|
+
int status = errno;
|
|
620
|
+
close(enumeration_descriptor);
|
|
621
|
+
return status;
|
|
622
|
+
}
|
|
623
|
+
int visible_entries = 0;
|
|
624
|
+
bool only_application = true;
|
|
625
|
+
for (;;) {
|
|
626
|
+
errno = 0;
|
|
627
|
+
struct dirent *entry = readdir(directory);
|
|
628
|
+
if (entry == NULL) break;
|
|
629
|
+
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
|
|
630
|
+
visible_entries += 1;
|
|
631
|
+
if (strcmp(entry->d_name, "Recordings.app") != 0) only_application = false;
|
|
632
|
+
}
|
|
633
|
+
int status = errno;
|
|
634
|
+
closedir(directory);
|
|
635
|
+
if (status != 0) return status;
|
|
636
|
+
if (visible_entries != 1 || !only_application) return EPERM;
|
|
637
|
+
struct copy_budget budget = {0, 0};
|
|
638
|
+
status = copy_directory_at(
|
|
639
|
+
verifier_output_directory_descriptor,
|
|
640
|
+
root_candidate_directory_descriptor,
|
|
641
|
+
"Recordings.app",
|
|
642
|
+
verifier_user_id,
|
|
643
|
+
&budget
|
|
644
|
+
);
|
|
645
|
+
if (status == 0 &&
|
|
646
|
+
!recordings_descriptor_has_no_extended_acl(root_candidate_directory_descriptor)) {
|
|
647
|
+
status = EPERM;
|
|
648
|
+
}
|
|
649
|
+
if (status == 0 && fsync(root_candidate_directory_descriptor) != 0) {
|
|
650
|
+
status = errno != 0 ? errno : EIO;
|
|
651
|
+
}
|
|
652
|
+
return status;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
static int remove_tree_at(
|
|
656
|
+
int parent_descriptor,
|
|
657
|
+
const char *name,
|
|
658
|
+
unsigned int depth,
|
|
659
|
+
uint64_t *entry_budget
|
|
660
|
+
) {
|
|
661
|
+
if (depth > 128 || *entry_budget >= 200000) return EFBIG;
|
|
662
|
+
*entry_budget += 1;
|
|
663
|
+
struct stat named;
|
|
664
|
+
if (fstatat(parent_descriptor, name, &named, AT_SYMLINK_NOFOLLOW) != 0) {
|
|
665
|
+
return errno;
|
|
666
|
+
}
|
|
667
|
+
if (!S_ISDIR(named.st_mode)) {
|
|
668
|
+
return unlinkat(parent_descriptor, name, 0) == 0 ? 0 : errno;
|
|
669
|
+
}
|
|
670
|
+
int directory = openat(
|
|
671
|
+
parent_descriptor,
|
|
672
|
+
name,
|
|
673
|
+
O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW
|
|
674
|
+
);
|
|
675
|
+
if (directory < 0) return errno;
|
|
676
|
+
struct stat opened;
|
|
677
|
+
if (fstat(directory, &opened) != 0 || named.st_dev != opened.st_dev ||
|
|
678
|
+
named.st_ino != opened.st_ino ||
|
|
679
|
+
!recordings_descriptor_has_no_extended_acl(directory)) {
|
|
680
|
+
close(directory);
|
|
681
|
+
return EBUSY;
|
|
682
|
+
}
|
|
683
|
+
int enumeration_descriptor = duplicate_at_least(directory, 10);
|
|
684
|
+
DIR *entries = enumeration_descriptor < 0 ? NULL : fdopendir(enumeration_descriptor);
|
|
685
|
+
int status = entries == NULL ? (errno != 0 ? errno : EIO) : 0;
|
|
686
|
+
if (entries == NULL && enumeration_descriptor >= 0) close(enumeration_descriptor);
|
|
687
|
+
while (status == 0) {
|
|
688
|
+
errno = 0;
|
|
689
|
+
struct dirent *entry = readdir(entries);
|
|
690
|
+
if (entry == NULL) {
|
|
691
|
+
if (errno != 0) status = errno;
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
|
|
695
|
+
if (entry->d_name[0] == '\0' || strchr(entry->d_name, '/') != NULL) {
|
|
696
|
+
status = EPERM;
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
status = remove_tree_at(directory, entry->d_name, depth + 1, entry_budget);
|
|
700
|
+
}
|
|
701
|
+
if (entries != NULL) closedir(entries);
|
|
702
|
+
if (status == 0 && !recordings_descriptor_has_no_extended_acl(directory)) status = EPERM;
|
|
703
|
+
if (status == 0 && fsync(directory) != 0) status = errno != 0 ? errno : EIO;
|
|
704
|
+
close(directory);
|
|
705
|
+
if (status == 0 && unlinkat(parent_descriptor, name, AT_REMOVEDIR) != 0) status = errno;
|
|
706
|
+
if (status == 0 && fsync(parent_descriptor) != 0) status = errno != 0 ? errno : EIO;
|
|
707
|
+
return status;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
int recordings_remove_directory_tree_at(
|
|
711
|
+
int root_directory_descriptor,
|
|
712
|
+
const char *directory_name
|
|
713
|
+
) {
|
|
714
|
+
if (root_directory_descriptor < 0 || !is_valid_transaction_name(directory_name)) {
|
|
715
|
+
return EINVAL;
|
|
716
|
+
}
|
|
717
|
+
struct stat root;
|
|
718
|
+
if (fstat(root_directory_descriptor, &root) != 0 || !S_ISDIR(root.st_mode) ||
|
|
719
|
+
root.st_uid != 0 || (root.st_mode & 0077) != 0 ||
|
|
720
|
+
!recordings_descriptor_has_no_extended_acl(root_directory_descriptor)) {
|
|
721
|
+
return EPERM;
|
|
722
|
+
}
|
|
723
|
+
uint64_t entry_budget = 0;
|
|
724
|
+
return remove_tree_at(root_directory_descriptor, directory_name, 0, &entry_budget);
|
|
725
|
+
}
|