@hasna/recordings 0.2.14 → 0.3.2
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/Dockerfile.package +4 -5
- package/README.md +113 -8
- package/bun.lock +2 -2
- package/dist/__tests__/helpers/installer-guard-execution.d.ts +22 -0
- package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +1 -0
- package/dist/__tests__/helpers/installer-preflight.d.ts +22 -0
- package/dist/__tests__/helpers/installer-preflight.d.ts.map +1 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -1
- package/dist/__tests__/helpers/source-assertions.d.ts +169 -0
- package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -0
- package/dist/__tests__/preload.d.ts +1 -4
- package/dist/__tests__/preload.d.ts.map +1 -1
- package/dist/cli/desktop-snapshot.d.ts +20 -0
- package/dist/cli/desktop-snapshot.d.ts.map +1 -0
- package/dist/cli/index.js +2094 -280
- package/dist/cli/macos-permissions.d.ts +72 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -1
- package/dist/cli/macos-shortcut.d.ts +139 -0
- package/dist/cli/macos-shortcut.d.ts.map +1 -0
- package/dist/cli/trigger-probe.d.ts +172 -0
- package/dist/cli/trigger-probe.d.ts.map +1 -0
- package/dist/db/database.d.ts +9 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/http/client.d.ts +19 -7
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +635 -50
- package/dist/lib/capture-probe.d.ts +216 -0
- package/dist/lib/capture-probe.d.ts.map +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/macos-bundle.d.ts +50 -0
- package/dist/lib/macos-bundle.d.ts.map +1 -0
- package/dist/lib/persistence-probe.d.ts +208 -0
- package/dist/lib/persistence-probe.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/retired-deployment-modes.d.ts +24 -0
- package/dist/lib/retired-deployment-modes.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts +60 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.js +141 -57
- package/dist/server/cloud-config.d.ts +25 -3
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud-readiness.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +8 -7
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +188 -80
- package/dist/server/migrate-command.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +109 -50
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +25 -6
- package/scripts/ci-linux-suite.ts +403 -0
- package/scripts/ci-native-build.ts +402 -0
- package/scripts/enforce_identity_migration.sh +110 -0
- package/scripts/generate-sdk.ts +1 -1
- package/scripts/install_macos_app.sh +192 -31
- package/scripts/macos_artifact.ts +323 -43
- package/scripts/native/realtime-settle-probe/README.md +39 -0
- package/scripts/native/realtime-settle-probe/main.swift +135 -0
- package/scripts/policy/local-only-approved-targets.txt +21 -0
- package/scripts/read_local_only_targets.sh +203 -0
- package/scripts/scan-artifact.ts +40 -0
- package/scripts/set-version.ts +145 -0
- package/scripts/smoke_macos_app.sh +62 -16
- package/scripts/vacuity-manifest-gen.ts +96 -0
- package/scripts/vacuity-manifests/enhancement-screen-mirror.tsv +44 -0
- package/scripts/vacuity-manifests/install-chain.tsv +13 -0
- package/scripts/vacuity-manifests/must-survive-controls.tsv +10 -0
- package/scripts/vacuity-manifests/reorder.tsv +4 -0
- package/scripts/vacuity-manifests/source-side.tsv +15 -0
- package/scripts/vacuity-manifests/variable-operands.tsv +16 -0
- package/scripts/vacuity-manifests/version-sites.tsv +61 -0
- package/scripts/vacuity-mutation-battery.ts +266 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +23 -11
- package/src/native/Recordings/App/RecordingsApp.swift +37 -1
- package/src/native/Recordings/App/RuntimeSmoke.swift +5 -1
- package/src/native/Recordings/App/SidebarView.swift +1 -1
- package/src/native/Recordings/Package.swift +24 -5
- package/src/native/Recordings/RecordingsLib/EnhancementScreen.swift +91 -0
- package/src/native/Recordings/RecordingsLib/FnKeyMonitor.swift +27 -4
- package/src/native/Recordings/RecordingsLib/Info.plist +2 -2
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +56 -3
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +13 -0
- package/src/native/Recordings/RecordingsLib/PasteDeliveryVerification.swift +460 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +21 -1
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +41 -7
- package/src/native/Recordings/RecordingsLib/RecordingAttemptAlert.swift +36 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +1071 -75
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +47 -3
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +11 -6
- package/src/native/Recordings/RecordingsTests/BarOnlyLaunchPlanTests.swift +99 -0
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +3 -3
- package/src/native/Recordings/RecordingsTests/EnhancementScreenTests.swift +191 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +144 -4
- package/src/native/Recordings/RecordingsTests/PasteDeliveryVerificationTests.swift +463 -0
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +33 -20
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RealtimeSettleBenchmark.swift +200 -0
- package/src/native/Recordings/RecordingsTests/RealtimeSettleBudgetTests.swift +80 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +22 -4
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +12 -6
- package/src/native/Recordings/RecordingsTests/RecordingEngineHomeIsolationTests.swift +67 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +12 -3
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +207 -11
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +17 -6
- package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +23 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +7 -1
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +10 -10
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +5 -1
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +1 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +59 -3
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +21 -2
- package/src/native/Recordings/Updater/ProtocolTests/ExpectationIntegrityTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/UpdateXPCInterfaceTests.swift +113 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +106 -5
- package/src/native/Recordings/build.sh +226 -20
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import Testing
|
|
2
|
+
|
|
3
|
+
/// Positive and negative controls for the test framework itself.
|
|
4
|
+
///
|
|
5
|
+
/// Package.swift pins swift-testing to a revision that does not match the compiler, because the
|
|
6
|
+
/// matching 6.3.x line hard-links a toolchain library that is not on SwiftPM's search path. The
|
|
7
|
+
/// rule that pin was standing in for — "the shim must track the compiler" — exists because a
|
|
8
|
+
/// mismatched shim once corrupted `#expect` Bool-comparison evaluation and passed false
|
|
9
|
+
/// assertions. A silently-passing `#expect` makes every other test in this package meaningless,
|
|
10
|
+
/// and no green run can reveal it.
|
|
11
|
+
///
|
|
12
|
+
/// These assert the property directly. The negative controls matter more than the positive ones:
|
|
13
|
+
/// `withKnownIssue` fails when its body records *no* issue, so if `#expect` ever stops evaluating
|
|
14
|
+
/// a false comparison, this suite goes red instead of the whole suite going quietly green.
|
|
15
|
+
struct ExpectationIntegrityTests {
|
|
16
|
+
@Test("#expect admits true Bool comparisons")
|
|
17
|
+
func admitsTrueComparisons() {
|
|
18
|
+
#expect(true == true)
|
|
19
|
+
#expect(false == false)
|
|
20
|
+
#expect(true != false)
|
|
21
|
+
#expect((1 == 1) == true)
|
|
22
|
+
#expect((1 == 2) == false)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test("#expect records an issue for a false Bool comparison")
|
|
26
|
+
func recordsIssueForFalseEquality() {
|
|
27
|
+
withKnownIssue("a false Bool comparison must be recorded, not passed") {
|
|
28
|
+
#expect(false == true)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Test("#expect records an issue for a false Bool inequality")
|
|
33
|
+
func recordsIssueForFalseInequality() {
|
|
34
|
+
withKnownIssue("an untrue inequality must be recorded, not passed") {
|
|
35
|
+
#expect(true != true)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test("#expect records an issue for a false comparison behind a variable")
|
|
40
|
+
func recordsIssueForFalseComparisonThroughVariables() {
|
|
41
|
+
// The reported corruption involved comparison *evaluation*, so route the operands
|
|
42
|
+
// through variables rather than literals the optimiser can fold away.
|
|
43
|
+
let left = Int.random(in: 1...1)
|
|
44
|
+
let right = left + 1
|
|
45
|
+
#expect(left != right)
|
|
46
|
+
withKnownIssue("a false comparison between variables must be recorded") {
|
|
47
|
+
#expect(left == right)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Test("#require unwraps a present optional")
|
|
52
|
+
func requireUnwrapsPresentOptional() throws {
|
|
53
|
+
// Only the positive direction is asserted here. The negative direction — `#require(nil)`
|
|
54
|
+
// — throws by design, and wrapping it in `withKnownIssue` additionally records an
|
|
55
|
+
// "API was misused" issue, so it adds noise rather than coverage. The Bool-comparison
|
|
56
|
+
// controls above are what the reported corruption was actually about.
|
|
57
|
+
let present: Int? = 7
|
|
58
|
+
#expect(try #require(present) == 7)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Testing
|
|
3
|
+
|
|
4
|
+
@testable import RecordingsUpdateProtocol
|
|
5
|
+
|
|
6
|
+
/// The XPC allowlist is the decoding trust boundary for the privileged updater, so these
|
|
7
|
+
/// assert the *exact* set installed for every argument rather than a subset. A set that
|
|
8
|
+
/// grew silently would widen what the broker is willing to decode; a set that shrank
|
|
9
|
+
/// would reject legitimate replies. Either direction should fail the build.
|
|
10
|
+
///
|
|
11
|
+
/// Membership is compared by class name because Objective-C class objects cannot be cast
|
|
12
|
+
/// to `AnyHashable` in Swift — `NSDictionary.self as! AnyHashable` traps at runtime.
|
|
13
|
+
struct UpdateXPCInterfaceTests {
|
|
14
|
+
private static let installSelector =
|
|
15
|
+
NSSelectorFromString("installWithArchive:manifest:envelope:reply:")
|
|
16
|
+
private static let statusSelector = NSSelectorFromString("queryStatusWithReply:")
|
|
17
|
+
|
|
18
|
+
private func allowedClassNames(
|
|
19
|
+
_ interface: NSXPCInterface,
|
|
20
|
+
selector: Selector,
|
|
21
|
+
argumentIndex: Int,
|
|
22
|
+
ofReply: Bool
|
|
23
|
+
) -> Set<String> {
|
|
24
|
+
let classes = interface.classes(for: selector, argumentIndex: argumentIndex, ofReply: ofReply)
|
|
25
|
+
return Set(classes.compactMap { ($0 as? AnyClass).map { NSStringFromClass($0) } })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Test("every install request argument accepts only a file handle")
|
|
29
|
+
func installRequestAcceptsOnlyFileHandles() {
|
|
30
|
+
let interface = makeRecordingsUpdateXPCInterface()
|
|
31
|
+
for index in 0..<3 {
|
|
32
|
+
let allowed = allowedClassNames(
|
|
33
|
+
interface,
|
|
34
|
+
selector: Self.installSelector,
|
|
35
|
+
argumentIndex: index,
|
|
36
|
+
ofReply: false
|
|
37
|
+
)
|
|
38
|
+
#expect(allowed == ["NSFileHandle"], "install request argument \(index)")
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test("install and status replies accept only scalar dictionaries")
|
|
43
|
+
func repliesAcceptOnlyScalarDictionaries() {
|
|
44
|
+
let interface = makeRecordingsUpdateXPCInterface()
|
|
45
|
+
let expected: Set<String> = ["NSDictionary", "NSString", "NSNumber"]
|
|
46
|
+
for selector in [Self.installSelector, Self.statusSelector] {
|
|
47
|
+
let allowed = allowedClassNames(
|
|
48
|
+
interface,
|
|
49
|
+
selector: selector,
|
|
50
|
+
argumentIndex: 0,
|
|
51
|
+
ofReply: true
|
|
52
|
+
)
|
|
53
|
+
#expect(allowed == expected, "reply allowlist for \(selector)")
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test("the reply allowlist admits no container beyond the dictionary itself")
|
|
58
|
+
func replyAllowlistExcludesNestedContainers() {
|
|
59
|
+
let interface = makeRecordingsUpdateXPCInterface()
|
|
60
|
+
let allowed = allowedClassNames(
|
|
61
|
+
interface,
|
|
62
|
+
selector: Self.statusSelector,
|
|
63
|
+
argumentIndex: 0,
|
|
64
|
+
ofReply: true
|
|
65
|
+
)
|
|
66
|
+
for rejected in ["NSArray", "NSData", "NSURL", "NSValue", "NSSet"] {
|
|
67
|
+
#expect(!allowed.contains(rejected), "\(rejected) must not be decodable in a reply")
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// The allowlist only matters if it is actually sufficient to decode what the broker
|
|
72
|
+
/// sends. Rather than comparing class names — a native Swift `String` key bridges to
|
|
73
|
+
/// `Swift.__StringStorage`, not `NSString`, so name matching tests the bridge instead of
|
|
74
|
+
/// the contract — this round-trips real replies through secure coding restricted to the
|
|
75
|
+
/// installed allowlist. A reply that grew a type XPC would refuse to decode fails here.
|
|
76
|
+
@Test("the installed allowlist can securely decode the replies the broker actually sends")
|
|
77
|
+
func allowlistDecodesRealReplies() throws {
|
|
78
|
+
let interface = makeRecordingsUpdateXPCInterface()
|
|
79
|
+
let allowedClasses = interface
|
|
80
|
+
.classes(for: Self.statusSelector, argumentIndex: 0, ofReply: true)
|
|
81
|
+
.compactMap { $0 as? AnyClass }
|
|
82
|
+
#expect(allowedClasses.count == 3)
|
|
83
|
+
|
|
84
|
+
let replies = [
|
|
85
|
+
updateSuccessReply(
|
|
86
|
+
transactionID: "t-1",
|
|
87
|
+
releaseID: "r-1",
|
|
88
|
+
installedVersion: "0.2.15"
|
|
89
|
+
),
|
|
90
|
+
updateFailureReply(.invalidEnvelope, message: "rejected"),
|
|
91
|
+
]
|
|
92
|
+
for reply in replies {
|
|
93
|
+
let data = try NSKeyedArchiver.archivedData(
|
|
94
|
+
withRootObject: reply,
|
|
95
|
+
requiringSecureCoding: true
|
|
96
|
+
)
|
|
97
|
+
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
|
|
98
|
+
unarchiver.requiresSecureCoding = true
|
|
99
|
+
let decoded = unarchiver.decodeObject(
|
|
100
|
+
of: allowedClasses,
|
|
101
|
+
forKey: NSKeyedArchiveRootObjectKey
|
|
102
|
+
)
|
|
103
|
+
unarchiver.finishDecoding()
|
|
104
|
+
#expect(unarchiver.error == nil, "reply is not decodable under the allowlist")
|
|
105
|
+
let decodedReply = try #require(decoded as? NSDictionary)
|
|
106
|
+
#expect(decodedReply.count == reply.count)
|
|
107
|
+
#expect(
|
|
108
|
+
decodedReply[RecordingsUpdateReplyKey.lifecycle] as? String
|
|
109
|
+
== RecordingsUpdateConstants.lifecycle
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
#include <sys/acl.h>
|
|
17
17
|
#include <sys/resource.h>
|
|
18
18
|
#include <sys/stat.h>
|
|
19
|
+
#include <sys/sysctl.h>
|
|
19
20
|
#include <sys/types.h>
|
|
20
21
|
#include <sys/wait.h>
|
|
21
22
|
#include <time.h>
|
|
@@ -29,6 +30,39 @@
|
|
|
29
30
|
#define OUTPUT_PATH_PREFIX "/Library/Application Support/Hasna/Recordings/Updates/transaction-"
|
|
30
31
|
#define OUTPUT_PATH_SUFFIX "/verifier-output"
|
|
31
32
|
|
|
33
|
+
/*
|
|
34
|
+
* Lower bound for the pre-exec descriptor sweep, so a pathologically small
|
|
35
|
+
* RLIMIT_NOFILE can never shrink the sweep below the descriptors we place by hand.
|
|
36
|
+
*/
|
|
37
|
+
#define DESCRIPTOR_SWEEP_FLOOR 64
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* PRIVATE API, DELIBERATE — RISK ACCEPTED.
|
|
41
|
+
*
|
|
42
|
+
* `sandbox_init_with_parameters` is exported by libSystem (`_sandbox_init_with_parameters`
|
|
43
|
+
* is present in libSystem.tbd) but is declared in no SDK header, so this declaration is
|
|
44
|
+
* the only contract we have. Apple may change its signature or remove it in any macOS
|
|
45
|
+
* update, and nothing checks our prototype against theirs.
|
|
46
|
+
*
|
|
47
|
+
* Why we accept that rather than avoid it: the public `sandbox_init` cannot express this
|
|
48
|
+
* policy. It is API_DEPRECATED since macOS 10.8, and its own header states that `flags`
|
|
49
|
+
* "Must be SANDBOX_NAMED" — it can only select one of the five built-in kSBXProfile*
|
|
50
|
+
* profiles, so it can neither load our profile nor bind the OUTPUT_DIR parameter the
|
|
51
|
+
* profile is written against. The alternative of textually substituting the output path
|
|
52
|
+
* into the profile source would move a runtime-derived string into a security policy
|
|
53
|
+
* document, which is strictly worse than depending on the parameter API built for it.
|
|
54
|
+
*
|
|
55
|
+
* Do NOT mark this `weak_import`. Referenced strongly, a symbol Apple removes becomes a
|
|
56
|
+
* link error at build time, which fails closed. Weak-linked, it would become a NULL call
|
|
57
|
+
* or a silently unsandboxed verifier child at runtime.
|
|
58
|
+
*/
|
|
59
|
+
extern int sandbox_init_with_parameters(
|
|
60
|
+
const char *profile,
|
|
61
|
+
uint64_t flags,
|
|
62
|
+
const char *const parameters[],
|
|
63
|
+
char **errorbuf
|
|
64
|
+
);
|
|
65
|
+
|
|
32
66
|
int recordings_descriptor_has_no_extended_acl(int descriptor) {
|
|
33
67
|
if (descriptor < 0) return 0;
|
|
34
68
|
errno = 0;
|
|
@@ -244,6 +278,57 @@ static bool apply_limit(int resource, rlim_t soft, rlim_t hard) {
|
|
|
244
278
|
return setrlimit(resource, &limit) == 0;
|
|
245
279
|
}
|
|
246
280
|
|
|
281
|
+
/*
|
|
282
|
+
* Highest descriptor number that could exist in this process, used to bound the pre-exec
|
|
283
|
+
* sweep below. MUST be evaluated in the parent, before the child lowers RLIMIT_NOFILE:
|
|
284
|
+
* verifier_child() drops RLIMIT_NOFILE to 32 before it sweeps, and lowering the limit does
|
|
285
|
+
* not close descriptors that are already open, so a getrlimit() call made after that point
|
|
286
|
+
* reports 32 and would skip every inherited descriptor at or above 32.
|
|
287
|
+
*/
|
|
288
|
+
static rlim_t inherited_descriptor_ceiling(void) {
|
|
289
|
+
rlim_t ceiling = 0;
|
|
290
|
+
struct rlimit limit;
|
|
291
|
+
if (getrlimit(RLIMIT_NOFILE, &limit) == 0) {
|
|
292
|
+
if (limit.rlim_cur != RLIM_INFINITY && limit.rlim_cur > ceiling) ceiling = limit.rlim_cur;
|
|
293
|
+
if (limit.rlim_max != RLIM_INFINITY && limit.rlim_max > ceiling) ceiling = limit.rlim_max;
|
|
294
|
+
}
|
|
295
|
+
/*
|
|
296
|
+
* An unlimited hard limit tells us nothing, so fall back to the kernel's per-process
|
|
297
|
+
* cap, which is the real ceiling on descriptor numbers.
|
|
298
|
+
*/
|
|
299
|
+
int per_process_maximum = 0;
|
|
300
|
+
size_t size = sizeof(per_process_maximum);
|
|
301
|
+
if (sysctlbyname("kern.maxfilesperproc", &per_process_maximum, &size, NULL, 0) == 0 &&
|
|
302
|
+
per_process_maximum > 0) {
|
|
303
|
+
if (ceiling == 0 || ceiling > (rlim_t)per_process_maximum) {
|
|
304
|
+
ceiling = (rlim_t)per_process_maximum;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (ceiling < DESCRIPTOR_SWEEP_FLOOR) ceiling = DESCRIPTOR_SWEEP_FLOOR;
|
|
308
|
+
return ceiling;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/*
|
|
312
|
+
* Darwin has neither `closefrom()` nor Linux's `close_range()` — both are absent from the
|
|
313
|
+
* macOS SDK entirely, not merely behind a header we failed to include — so the sweep is
|
|
314
|
+
* open-coded.
|
|
315
|
+
*
|
|
316
|
+
* A plain bounded loop of close() is used rather than enumerating /dev/fd, which would
|
|
317
|
+
* close exactly the open descriptors instead of probing a range. /dev/fd enumeration needs
|
|
318
|
+
* opendir()/readdir(), which allocate; this code runs after fork() in a multithreaded XPC
|
|
319
|
+
* broker, where malloc can deadlock if another thread held its lock at fork time. close()
|
|
320
|
+
* and getrlimit() are plain syscalls and are safe here, so correctness of the sandbox
|
|
321
|
+
* boundary is not traded for an allocation in the one place we must not allocate.
|
|
322
|
+
*
|
|
323
|
+
* The cost is bounded by `ceiling` failed close() calls once per verification, which is
|
|
324
|
+
* negligible against the caller's 90-second budget.
|
|
325
|
+
*/
|
|
326
|
+
static void close_descriptors_from(int lowest, rlim_t ceiling) {
|
|
327
|
+
for (rlim_t descriptor = (rlim_t)lowest; descriptor < ceiling; descriptor++) {
|
|
328
|
+
(void)close((int)descriptor);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
247
332
|
static void kill_and_reap(pid_t child) {
|
|
248
333
|
kill(child, SIGKILL);
|
|
249
334
|
int wait_status = 0;
|
|
@@ -257,7 +342,8 @@ static void verifier_child(
|
|
|
257
342
|
gid_t group_id,
|
|
258
343
|
const char *expected_sha256,
|
|
259
344
|
const char *sandbox_profile,
|
|
260
|
-
const char *output_path
|
|
345
|
+
const char *output_path,
|
|
346
|
+
rlim_t descriptor_ceiling
|
|
261
347
|
) {
|
|
262
348
|
if (!apply_limit(RLIMIT_CPU, 60, 60) ||
|
|
263
349
|
!apply_limit(RLIMIT_AS, 4ULL * 1024 * 1024 * 1024, 4ULL * 1024 * 1024 * 1024) ||
|
|
@@ -276,7 +362,7 @@ static void verifier_child(
|
|
|
276
362
|
_exit(74);
|
|
277
363
|
}
|
|
278
364
|
if (null_descriptor > CHILD_OUTPUT_FD) close(null_descriptor);
|
|
279
|
-
|
|
365
|
+
close_descriptors_from(CHILD_OUTPUT_FD + 1, descriptor_ceiling);
|
|
280
366
|
|
|
281
367
|
gid_t groups[] = { group_id };
|
|
282
368
|
if (setgroups(1, groups) != 0 || setgid(group_id) != 0 || setuid(user_id) != 0 ||
|
|
@@ -289,12 +375,23 @@ static void verifier_child(
|
|
|
289
375
|
};
|
|
290
376
|
char *sandbox_error = NULL;
|
|
291
377
|
if (sandbox_init_with_parameters(sandbox_profile, 0, sandbox_parameters, &sandbox_error) != 0) {
|
|
292
|
-
if (sandbox_error != NULL)
|
|
378
|
+
if (sandbox_error != NULL) {
|
|
379
|
+
// `sandbox_free_error` is the only documented way to release the buffer the
|
|
380
|
+
// sandbox API allocates, and it is deprecated with no replacement. We _exit
|
|
381
|
+
// immediately below, so this is belt-and-braces rather than load-bearing;
|
|
382
|
+
// the deprecation warning is scoped to this one call instead of the file.
|
|
383
|
+
#pragma clang diagnostic push
|
|
384
|
+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
385
|
+
sandbox_free_error(sandbox_error);
|
|
386
|
+
#pragma clang diagnostic pop
|
|
387
|
+
}
|
|
293
388
|
_exit(72);
|
|
294
389
|
}
|
|
295
390
|
// Sandbox initialization is allowed to use implementation-internal descriptors.
|
|
296
391
|
// Re-close everything above the two explicit artifact descriptors before exec.
|
|
297
|
-
|
|
392
|
+
// RLIMIT_NOFILE is 32 by this point, so the sandbox cannot have opened anything above
|
|
393
|
+
// it; sweeping to the inherited ceiling again is generous and harmless.
|
|
394
|
+
close_descriptors_from(CHILD_OUTPUT_FD + 1, descriptor_ceiling);
|
|
298
395
|
|
|
299
396
|
char archive_argument[16];
|
|
300
397
|
char output_argument[16];
|
|
@@ -356,6 +453,9 @@ int recordings_run_artifact_verifier(
|
|
|
356
453
|
return errno != 0 ? errno : EMFILE;
|
|
357
454
|
}
|
|
358
455
|
|
|
456
|
+
/* Captured before fork(), while getrlimit() still describes the real descriptor table. */
|
|
457
|
+
const rlim_t descriptor_ceiling = inherited_descriptor_ceiling();
|
|
458
|
+
|
|
359
459
|
const pid_t child = fork();
|
|
360
460
|
if (child < 0) {
|
|
361
461
|
status = errno;
|
|
@@ -367,7 +467,8 @@ int recordings_run_artifact_verifier(
|
|
|
367
467
|
verifier_group_id,
|
|
368
468
|
expected_archive_sha256,
|
|
369
469
|
sandbox_profile,
|
|
370
|
-
output_path
|
|
470
|
+
output_path,
|
|
471
|
+
descriptor_ceiling
|
|
371
472
|
);
|
|
372
473
|
} else {
|
|
373
474
|
struct timespec started;
|