owlrunkit 0.25.0 → 0.25.1

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.
@@ -1,11 +1,9 @@
1
1
  import {
2
2
  existsSync,
3
3
  lstatSync,
4
- mkdirSync,
5
4
  readFileSync,
6
5
  readdirSync,
7
6
  realpathSync,
8
- rmSync,
9
7
  } from "node:fs";
10
8
  import path from "node:path";
11
9
 
@@ -16,6 +14,7 @@ import {
16
14
  validateExecutionPin,
17
15
  validateLeaseOwnedPaths,
18
16
  } from "./core-contract.mjs";
17
+ import { acquireRunKitControlLock } from "./control-transaction-lock.mjs";
19
18
  import {
20
19
  assertAllowedKeys,
21
20
  loadActiveExecution,
@@ -179,33 +178,25 @@ export function assertExecutionUnclosed(executionRoot, runId) {
179
178
  }
180
179
  }
181
180
 
182
- function acquireControlLock(workspaceRoot) {
181
+ function acquireControlLock(workspaceRoot, options) {
183
182
  const runtimeRoot = path.join(workspaceRoot, RUNTIME_ROOT);
184
183
  safeDirectory(workspaceRoot, runtimeRoot, "RunKit runtime directory");
185
184
  safeDirectory(runtimeRoot, path.join(runtimeRoot, "executions"), "Executions directory");
186
- const lockPath = path.join(runtimeRoot, "control.lock");
187
- try {
188
- mkdirSync(lockPath);
189
- } catch (error) {
190
- if (error?.code === "EEXIST") throw new Error("Another RunKit control transaction is already active.");
191
- throw error;
192
- }
193
- return () => rmSync(lockPath, { recursive: true, force: true });
185
+ return acquireRunKitControlLock(runtimeRoot, options);
194
186
  }
195
187
 
196
- export function withControlTransaction(workspaceRoot, operation) {
197
- const release = acquireControlLock(workspaceRoot);
188
+ export function withControlTransaction(workspaceRoot, operation, options) {
189
+ const release = acquireControlLock(workspaceRoot, options);
190
+ let result;
198
191
  try {
199
- const result = operation();
200
- if (result && typeof result.then === "function") {
201
- return Promise.resolve(result).finally(release);
202
- }
203
- release();
204
- return result;
192
+ result = operation();
193
+ if (result && typeof result.then === "function") return Promise.resolve(result).finally(release);
205
194
  } catch (error) {
206
195
  release();
207
196
  throw error;
208
197
  }
198
+ release();
199
+ return result;
209
200
  }
210
201
 
211
202
  export function acquireLeaseWithinControlTransaction({ workspaceRoot, runId, workItemId, ownedPaths }) {
@@ -251,7 +242,8 @@ export function acquireLeaseWithinControlTransaction({ workspaceRoot, runId, wor
251
242
  }
252
243
 
253
244
  export function acquireLease(args) {
254
- return withControlTransaction(args.workspaceRoot, () => acquireLeaseWithinControlTransaction(args));
245
+ return withControlTransaction(args.workspaceRoot, () => acquireLeaseWithinControlTransaction(args),
246
+ { atomicLease: true });
255
247
  }
256
248
 
257
249
  export function inspectLeases({ workspaceRoot, runId }) {
@@ -300,7 +292,8 @@ export function releaseLeaseWithinControlTransaction({ workspaceRoot, runId, wor
300
292
  }
301
293
 
302
294
  export function releaseLease(args) {
303
- return withControlTransaction(args.workspaceRoot, () => releaseLeaseWithinControlTransaction(args));
295
+ return withControlTransaction(args.workspaceRoot, () => releaseLeaseWithinControlTransaction(args),
296
+ { atomicLease: true });
304
297
  }
305
298
 
306
299
  export function restoreLeaseWithinControlTransaction({ workspaceRoot, runId, workItemId }) {
@@ -0,0 +1,13 @@
1
+ import { lstatSync, realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function linuxRuntimeHelper() {
5
+ if (process.platform !== "linux" || !["arm64", "x64"].includes(process.arch)) return null;
6
+ const executable = path.join(import.meta.dirname, `peer-linux-${process.arch}`);
7
+ try {
8
+ if (realpathSync(executable) === executable && lstatSync(executable).isFile()) return executable;
9
+ } catch (error) {
10
+ if (error.code !== "ENOENT") throw error;
11
+ }
12
+ return null;
13
+ }
@@ -0,0 +1,193 @@
1
+ musl as a whole is licensed under the following standard MIT license:
2
+
3
+ ----------------------------------------------------------------------
4
+ Copyright © 2005-2020 Rich Felker, et al.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ ----------------------------------------------------------------------
25
+
26
+ Authors/contributors include:
27
+
28
+ A. Wilcox
29
+ Ada Worcester
30
+ Alex Dowad
31
+ Alex Suykov
32
+ Alexander Monakov
33
+ Andre McCurdy
34
+ Andrew Kelley
35
+ Anthony G. Basile
36
+ Aric Belsito
37
+ Arvid Picciani
38
+ Bartosz Brachaczek
39
+ Benjamin Peterson
40
+ Bobby Bingham
41
+ Boris Brezillon
42
+ Brent Cook
43
+ Chris Spiegel
44
+ Clément Vasseur
45
+ Daniel Micay
46
+ Daniel Sabogal
47
+ Daurnimator
48
+ David Carlier
49
+ David Edelsohn
50
+ Denys Vlasenko
51
+ Dmitry Ivanov
52
+ Dmitry V. Levin
53
+ Drew DeVault
54
+ Emil Renner Berthing
55
+ Fangrui Song
56
+ Felix Fietkau
57
+ Felix Janda
58
+ Gianluca Anzolin
59
+ Hauke Mehrtens
60
+ He X
61
+ Hiltjo Posthuma
62
+ Isaac Dunham
63
+ Jaydeep Patil
64
+ Jens Gustedt
65
+ Jeremy Huntwork
66
+ Jo-Philipp Wich
67
+ Joakim Sindholt
68
+ John Spencer
69
+ Julien Ramseier
70
+ Justin Cormack
71
+ Kaarle Ritvanen
72
+ Khem Raj
73
+ Kylie McClain
74
+ Leah Neukirchen
75
+ Luca Barbato
76
+ Luka Perkov
77
+ M Farkas-Dyck (Strake)
78
+ Mahesh Bodapati
79
+ Markus Wichmann
80
+ Masanori Ogino
81
+ Michael Clark
82
+ Michael Forney
83
+ Mikhail Kremnyov
84
+ Natanael Copa
85
+ Nicholas J. Kain
86
+ orc
87
+ Pascal Cuoq
88
+ Patrick Oppenlander
89
+ Petr Hosek
90
+ Petr Skocik
91
+ Pierre Carrier
92
+ Reini Urban
93
+ Rich Felker
94
+ Richard Pennington
95
+ Ryan Fairfax
96
+ Samuel Holland
97
+ Segev Finer
98
+ Shiz
99
+ sin
100
+ Solar Designer
101
+ Stefan Kristiansson
102
+ Stefan O'Rear
103
+ Szabolcs Nagy
104
+ Timo Teräs
105
+ Trutz Behn
106
+ Valentin Ochs
107
+ Will Dietz
108
+ William Haddon
109
+ William Pitcock
110
+
111
+ Portions of this software are derived from third-party works licensed
112
+ under terms compatible with the above MIT license:
113
+
114
+ The TRE regular expression implementation (src/regex/reg* and
115
+ src/regex/tre*) is Copyright © 2001-2008 Ville Laurikari and licensed
116
+ under a 2-clause BSD license (license text in the source files). The
117
+ included version has been heavily modified by Rich Felker in 2012, in
118
+ the interests of size, simplicity, and namespace cleanliness.
119
+
120
+ Much of the math library code (src/math/* and src/complex/*) is
121
+ Copyright © 1993,2004 Sun Microsystems or
122
+ Copyright © 2003-2011 David Schultz or
123
+ Copyright © 2003-2009 Steven G. Kargl or
124
+ Copyright © 2003-2009 Bruce D. Evans or
125
+ Copyright © 2008 Stephen L. Moshier or
126
+ Copyright © 2017-2018 Arm Limited
127
+ and labelled as such in comments in the individual source files. All
128
+ have been licensed under extremely permissive terms.
129
+
130
+ The ARM memcpy code (src/string/arm/memcpy.S) is Copyright © 2008
131
+ The Android Open Source Project and is licensed under a two-clause BSD
132
+ license. It was taken from Bionic libc, used on Android.
133
+
134
+ The AArch64 memcpy and memset code (src/string/aarch64/*) are
135
+ Copyright © 1999-2019, Arm Limited.
136
+
137
+ The implementation of DES for crypt (src/crypt/crypt_des.c) is
138
+ Copyright © 1994 David Burren. It is licensed under a BSD license.
139
+
140
+ The implementation of blowfish crypt (src/crypt/crypt_blowfish.c) was
141
+ originally written by Solar Designer and placed into the public
142
+ domain. The code also comes with a fallback permissive license for use
143
+ in jurisdictions that may not recognize the public domain.
144
+
145
+ The smoothsort implementation (src/stdlib/qsort.c) is Copyright © 2011
146
+ Valentin Ochs and is licensed under an MIT-style license.
147
+
148
+ The x86_64 port was written by Nicholas J. Kain and is licensed under
149
+ the standard MIT terms.
150
+
151
+ The mips and microblaze ports were originally written by Richard
152
+ Pennington for use in the ellcc project. The original code was adapted
153
+ by Rich Felker for build system and code conventions during upstream
154
+ integration. It is licensed under the standard MIT terms.
155
+
156
+ The mips64 port was contributed by Imagination Technologies and is
157
+ licensed under the standard MIT terms.
158
+
159
+ The powerpc port was also originally written by Richard Pennington,
160
+ and later supplemented and integrated by John Spencer. It is licensed
161
+ under the standard MIT terms.
162
+
163
+ All other files which have no copyright comments are original works
164
+ produced specifically for use as part of this library, written either
165
+ by Rich Felker, the main author of the library, or by one or more
166
+ contibutors listed above. Details on authorship of individual files
167
+ can be found in the git version control history of the project. The
168
+ omission of copyright and license comments in each file is in the
169
+ interest of source tree size.
170
+
171
+ In addition, permission is hereby granted for all public header files
172
+ (include/* and arch/*/bits/*) and crt files intended to be linked into
173
+ applications (crt/*, ldso/dlstart.c, and arch/*/crt_arch.h) to omit
174
+ the copyright notice and permission notice otherwise required by the
175
+ license, and to use these files without any requirement of
176
+ attribution. These files include substantial contributions from:
177
+
178
+ Bobby Bingham
179
+ John Spencer
180
+ Nicholas J. Kain
181
+ Rich Felker
182
+ Richard Pennington
183
+ Stefan Kristiansson
184
+ Szabolcs Nagy
185
+
186
+ all of whom have explicitly granted such permission.
187
+
188
+ This file previously contained text expressing a belief that most of
189
+ the files covered by the above exception were sufficiently trivial not
190
+ to be subject to copyright, resulting in confusion over whether it
191
+ negated the permissions granted in the license. In the spirit of
192
+ permissive licensing, and of not having licensing issues being an
193
+ obstacle to adoption, that text has been removed.
@@ -0,0 +1,243 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ #define _GNU_SOURCE
3
+ #include <errno.h>
4
+ #include <fcntl.h>
5
+ #include <linux/audit.h>
6
+ #include <linux/filter.h>
7
+ #include <linux/landlock.h>
8
+ #include <linux/seccomp.h>
9
+ #include <sched.h>
10
+ #include <signal.h>
11
+ #include <stddef.h>
12
+ #include <stdint.h>
13
+ #include <stdio.h>
14
+ #include <stdlib.h>
15
+ #include <string.h>
16
+ #include <sys/file.h>
17
+ #include <sys/ioctl.h>
18
+ #include <sys/prctl.h>
19
+ #include <sys/socket.h>
20
+ #include <sys/stat.h>
21
+ #include <sys/syscall.h>
22
+ #include <sys/xattr.h>
23
+ #include <unistd.h>
24
+
25
+ // ABI 3 added this bit. The runtime still checks that the kernel supports it.
26
+ #ifndef LANDLOCK_ACCESS_FS_TRUNCATE
27
+ #define LANDLOCK_ACCESS_FS_TRUNCATE (1ULL << 14)
28
+ #endif
29
+
30
+ #if defined(__aarch64__)
31
+ #define NATIVE_AUDIT_ARCH AUDIT_ARCH_AARCH64
32
+ #define GLIBC_LOADER "/lib/ld-linux-aarch64.so.1"
33
+ #define MUSL_LOADER "/lib/ld-musl-aarch64.so.1"
34
+ #elif defined(__x86_64__) && !defined(__ILP32__)
35
+ #define NATIVE_AUDIT_ARCH AUDIT_ARCH_X86_64
36
+ #define GLIBC_LOADER "/lib64/ld-linux-x86-64.so.2"
37
+ #define MUSL_LOADER "/lib/ld-musl-x86_64.so.1"
38
+ #else
39
+ #error Unsupported OwlRunKit peer architecture
40
+ #endif
41
+
42
+ extern char **environ;
43
+
44
+ static void fail(const char *operation)
45
+ {
46
+ perror(operation);
47
+ _exit(125);
48
+ }
49
+
50
+ static void allow_path(int ruleset, const char *path, uint64_t rights, int optional)
51
+ {
52
+ int fd = open(path, O_PATH | O_CLOEXEC);
53
+ if (fd < 0) {
54
+ if (optional && errno == ENOENT)
55
+ return;
56
+ fail(path);
57
+ }
58
+ struct landlock_path_beneath_attr rule = { .allowed_access = rights, .parent_fd = fd };
59
+ if (syscall(SYS_landlock_add_rule, ruleset, LANDLOCK_RULE_PATH_BENEATH, &rule, 0) < 0)
60
+ fail("landlock_add_rule");
61
+ close(fd);
62
+ }
63
+
64
+ static void restrict_files(const char *directory, const char *worker)
65
+ {
66
+ const uint64_t read_tree = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
67
+ const uint64_t work = read_tree | LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_TRUNCATE |
68
+ LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_MAKE_DIR | LANDLOCK_ACCESS_FS_REMOVE_FILE |
69
+ LANDLOCK_ACCESS_FS_REMOVE_DIR | LANDLOCK_ACCESS_FS_REFER;
70
+ const struct landlock_ruleset_attr rules = { .handled_access_fs = work |
71
+ LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_MAKE_CHAR | LANDLOCK_ACCESS_FS_MAKE_BLOCK |
72
+ LANDLOCK_ACCESS_FS_MAKE_SOCK | LANDLOCK_ACCESS_FS_MAKE_FIFO | LANDLOCK_ACCESS_FS_MAKE_SYM };
73
+ long abi = syscall(SYS_landlock_create_ruleset, NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
74
+ if (abi < 0)
75
+ fail("landlock_create_ruleset version");
76
+ if (abi < 3) {
77
+ errno = EOPNOTSUPP;
78
+ fail("Landlock ABI 3 is required");
79
+ }
80
+ int fd = syscall(SYS_landlock_create_ruleset, &rules, sizeof rules, 0);
81
+ if (fd < 0)
82
+ fail("landlock_create_ruleset");
83
+ if (worker) {
84
+ allow_path(fd, directory, work, 0);
85
+ allow_path(fd, worker, LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_EXECUTE, 0);
86
+ allow_path(fd, "/lib", read_tree, 1);
87
+ allow_path(fd, "/lib64", read_tree, 1);
88
+ allow_path(fd, "/usr/lib", read_tree, 1);
89
+ allow_path(fd, "/usr/lib64", read_tree, 1);
90
+ allow_path(fd, GLIBC_LOADER, LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_EXECUTE, 1);
91
+ allow_path(fd, MUSL_LOADER, LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_EXECUTE, 1);
92
+ allow_path(fd, "/etc/ld.so.cache", LANDLOCK_ACCESS_FS_READ_FILE, 1);
93
+ allow_path(fd, "/dev/null", LANDLOCK_ACCESS_FS_READ_FILE, 1);
94
+ }
95
+ if (syscall(SYS_landlock_restrict_self, fd, 0) < 0)
96
+ fail("landlock_restrict_self");
97
+ close(fd);
98
+ }
99
+
100
+ #define ALLOW_SYSCALL(name) \
101
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_##name, 0, 1), \
102
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
103
+ #define DENIED (SECCOMP_RET_ERRNO | EPERM)
104
+
105
+ static void restrict_syscalls(void)
106
+ {
107
+ const unsigned thread_flags = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD |
108
+ CLONE_SYSVSEM | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_CHILD_SETTID;
109
+ const unsigned pid = (unsigned)getpid();
110
+ struct sock_filter instructions[] = {
111
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)),
112
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, NATIVE_AUDIT_ARCH, 1, 0),
113
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
114
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
115
+
116
+ // glibc retries via clone when clone3 is unavailable. Its pointer
117
+ // arguments cannot be safely checked by a seccomp BPF program.
118
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_clone3, 0, 1),
119
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | ENOSYS),
120
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_clone, 0, 6),
121
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])),
122
+ BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, CLONE_THREAD, 0, 3),
123
+ BPF_STMT(BPF_ALU | BPF_AND | BPF_K, ~thread_flags),
124
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 0, 1),
125
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
126
+ BPF_STMT(BPF_RET | BPF_K, DENIED),
127
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_tgkill, 0, 4),
128
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])),
129
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, pid, 0, 1),
130
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
131
+ BPF_STMT(BPF_RET | BPF_K, DENIED),
132
+
133
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_ioctl, 0, 6),
134
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])),
135
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, FIONBIO, 3, 0),
136
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, FIONREAD, 2, 0),
137
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, TCGETS, 1, 0),
138
+ BPF_STMT(BPF_RET | BPF_K, DENIED),
139
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
140
+
141
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_fcntl, 0, 11),
142
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1])),
143
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, F_GETFD, 8, 0),
144
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, F_SETFD, 7, 0),
145
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, F_GETFL, 6, 0),
146
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, F_DUPFD, 5, 0),
147
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, F_DUPFD_CLOEXEC, 4, 0),
148
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, F_SETFL, 0, 2),
149
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[2])),
150
+ BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, O_ASYNC, 0, 1),
151
+ BPF_STMT(BPF_RET | BPF_K, DENIED),
152
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
153
+
154
+ // Data and path operations below are subject to Landlock. Operations
155
+ // it cannot confine, such as chmod and xattrs, stay denied.
156
+ ALLOW_SYSCALL(execve),
157
+ ALLOW_SYSCALL(read), ALLOW_SYSCALL(write), ALLOW_SYSCALL(readv), ALLOW_SYSCALL(writev),
158
+ ALLOW_SYSCALL(pread64), ALLOW_SYSCALL(pwrite64), ALLOW_SYSCALL(lseek), ALLOW_SYSCALL(close),
159
+ ALLOW_SYSCALL(openat), ALLOW_SYSCALL(fstat), ALLOW_SYSCALL(newfstatat), ALLOW_SYSCALL(statx),
160
+ ALLOW_SYSCALL(faccessat), ALLOW_SYSCALL(faccessat2), ALLOW_SYSCALL(getdents64),
161
+ ALLOW_SYSCALL(getcwd), ALLOW_SYSCALL(chdir), ALLOW_SYSCALL(fchdir), ALLOW_SYSCALL(readlinkat),
162
+ ALLOW_SYSCALL(mkdirat), ALLOW_SYSCALL(unlinkat), ALLOW_SYSCALL(renameat),
163
+ ALLOW_SYSCALL(renameat2), ALLOW_SYSCALL(linkat), ALLOW_SYSCALL(symlinkat),
164
+ ALLOW_SYSCALL(truncate), ALLOW_SYSCALL(ftruncate), ALLOW_SYSCALL(fsync), ALLOW_SYSCALL(fdatasync),
165
+ ALLOW_SYSCALL(dup), ALLOW_SYSCALL(dup3), ALLOW_SYSCALL(pipe2),
166
+ ALLOW_SYSCALL(mmap), ALLOW_SYSCALL(munmap), ALLOW_SYSCALL(mprotect), ALLOW_SYSCALL(mremap),
167
+ ALLOW_SYSCALL(madvise), ALLOW_SYSCALL(brk),
168
+ ALLOW_SYSCALL(rt_sigaction), ALLOW_SYSCALL(rt_sigprocmask), ALLOW_SYSCALL(rt_sigreturn),
169
+ ALLOW_SYSCALL(sigaltstack), ALLOW_SYSCALL(getpid), ALLOW_SYSCALL(getppid), ALLOW_SYSCALL(gettid),
170
+ ALLOW_SYSCALL(getuid), ALLOW_SYSCALL(geteuid), ALLOW_SYSCALL(getgid), ALLOW_SYSCALL(getegid),
171
+ ALLOW_SYSCALL(getgroups), ALLOW_SYSCALL(uname), ALLOW_SYSCALL(sysinfo),
172
+ ALLOW_SYSCALL(set_tid_address), ALLOW_SYSCALL(set_robust_list), ALLOW_SYSCALL(rseq),
173
+ ALLOW_SYSCALL(getrandom), ALLOW_SYSCALL(futex), ALLOW_SYSCALL(sched_yield),
174
+ ALLOW_SYSCALL(sched_getaffinity), ALLOW_SYSCALL(clock_gettime),
175
+ ALLOW_SYSCALL(clock_nanosleep), ALLOW_SYSCALL(nanosleep), ALLOW_SYSCALL(gettimeofday),
176
+ ALLOW_SYSCALL(epoll_create1), ALLOW_SYSCALL(epoll_ctl), ALLOW_SYSCALL(epoll_pwait),
177
+ ALLOW_SYSCALL(epoll_pwait2), ALLOW_SYSCALL(eventfd2), ALLOW_SYSCALL(ppoll),
178
+ ALLOW_SYSCALL(getsockname), ALLOW_SYSCALL(getpeername), ALLOW_SYSCALL(getsockopt),
179
+ ALLOW_SYSCALL(getrlimit), ALLOW_SYSCALL(exit), ALLOW_SYSCALL(exit_group),
180
+ #ifdef __x86_64__
181
+ ALLOW_SYSCALL(arch_prctl), ALLOW_SYSCALL(access), ALLOW_SYSCALL(open),
182
+ ALLOW_SYSCALL(stat), ALLOW_SYSCALL(lstat), ALLOW_SYSCALL(readlink),
183
+ ALLOW_SYSCALL(mkdir), ALLOW_SYSCALL(rmdir), ALLOW_SYSCALL(unlink),
184
+ ALLOW_SYSCALL(rename), ALLOW_SYSCALL(link), ALLOW_SYSCALL(symlink),
185
+ ALLOW_SYSCALL(dup2), ALLOW_SYSCALL(pipe), ALLOW_SYSCALL(epoll_wait), ALLOW_SYSCALL(poll),
186
+ #endif
187
+ BPF_STMT(BPF_RET | BPF_K, DENIED),
188
+ };
189
+ const struct sock_fprog filter = {
190
+ .len = sizeof instructions / sizeof instructions[0], .filter = instructions,
191
+ };
192
+ if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &filter) < 0)
193
+ fail("seccomp filter");
194
+ }
195
+
196
+ int main(int argc, char **argv)
197
+ {
198
+ if (argc == 2 && strcmp(argv[1], "lock") == 0) {
199
+ if (flock(3, LOCK_EX | LOCK_NB) == 0)
200
+ return 0;
201
+ if (errno == EWOULDBLOCK)
202
+ return 73;
203
+ fail("control flock");
204
+ }
205
+ int probe = argc == 2 && strcmp(argv[1], "probe") == 0;
206
+ if (!probe && (argc < 5 || strcmp(argv[1], "run") != 0))
207
+ return 125;
208
+ if (!probe) {
209
+ char *end;
210
+ long parent = strtol(argv[2], &end, 10);
211
+ if (*end || parent <= 0 || prctl(PR_SET_PDEATHSIG, SIGKILL) < 0 || getppid() != parent)
212
+ return 125;
213
+ struct stat worker;
214
+ if (stat(argv[4], &worker) < 0)
215
+ fail("stat worker");
216
+ if (worker.st_mode & (S_ISUID | S_ISGID)) {
217
+ errno = EPERM;
218
+ fail("set-id workers are unsupported");
219
+ }
220
+ if (getxattr(argv[4], "security.capability", NULL, 0) >= 0) {
221
+ errno = EPERM;
222
+ fail("file capabilities on workers are unsupported");
223
+ }
224
+ if (errno != ENODATA && errno != ENOTSUP)
225
+ fail("getxattr worker capabilities");
226
+ }
227
+ if (syscall(SYS_close_range, 3U, ~0U, 0) < 0)
228
+ fail("close inherited descriptors");
229
+ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0)
230
+ fail("no_new_privs");
231
+ restrict_files(probe ? NULL : argv[3], probe ? NULL : argv[4]);
232
+ restrict_syscalls();
233
+ if (probe) {
234
+ if (open("/etc/passwd", O_RDONLY) >= 0 || errno != EACCES ||
235
+ socket(AF_INET, SOCK_STREAM, 0) >= 0 || errno != EPERM) {
236
+ fputs("Linux peer enforcement self-test failed\n", stderr);
237
+ return 125;
238
+ }
239
+ return 0;
240
+ }
241
+ execve(argv[4], &argv[4], environ);
242
+ fail("execve worker");
243
+ }
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { spawn } from "node:child_process";
2
+ import { spawn, spawnSync } from "node:child_process";
3
3
  import {
4
- closeSync, constants, existsSync, fsyncSync, linkSync, lstatSync, mkdirSync,
4
+ accessSync, closeSync, constants, existsSync, fsyncSync, linkSync, lstatSync, mkdirSync,
5
5
  mkdtempSync, openSync, readSync, readdirSync, realpathSync, unlinkSync,
6
6
  watch, writeFileSync,
7
7
  } from "node:fs";
@@ -9,6 +9,7 @@ import { tmpdir } from "node:os";
9
9
  import path from "node:path";
10
10
 
11
11
  import { RUNTIME_ROOT, currentCoreIdentity } from "./core-contract.mjs";
12
+ import { linuxRuntimeHelper } from "./linux-runtime.mjs";
12
13
  import { safeIdentifier } from "./provenance-common.mjs";
13
14
  import { resolveVerificationEnvelopeBackendV1 } from "./verification-envelope.mjs";
14
15
 
@@ -195,9 +196,29 @@ function projection(task, state) {
195
196
  };
196
197
  }
197
198
 
199
+ export function resolvePeerRecoveryBackend() {
200
+ if (process.platform === "linux") {
201
+ try {
202
+ const executable = linuxRuntimeHelper();
203
+ if (!executable) return { available: false };
204
+ const result = spawnSync(executable, ["probe"], {
205
+ timeout: 3000, env: {}, stdio: ["ignore", "ignore", "pipe"], encoding: "utf8",
206
+ });
207
+ if (result.status === 0) {
208
+ return { available: true, id: "linux_landlock_seccomp_v1", executable,
209
+ digest: digest(regularBytes(executable, CHECKPOINT_LIMIT)) };
210
+ }
211
+ return { available: false, reason: result.stderr?.trim() || result.error?.code };
212
+ } catch (error) { return { available: false, reason: error.code }; }
213
+ }
214
+ return resolveVerificationEnvelopeBackendV1();
215
+ }
216
+
198
217
  function requireBackend() {
199
- const backend = resolveVerificationEnvelopeBackendV1();
200
- if (!backend.available) throw new Error("peer_recovery_enforced_backend_unavailable");
218
+ const backend = resolvePeerRecoveryBackend();
219
+ if (!backend.available) {
220
+ throw new Error(["peer_recovery_enforced_backend_unavailable", backend.reason].filter(Boolean).join(": "));
221
+ }
201
222
  return backend;
202
223
  }
203
224
 
@@ -357,34 +378,57 @@ function reportExit(task, attemptId, processExit) {
357
378
  }
358
379
  }
359
380
 
360
- export async function runPeerTask({ workspaceRoot, taskId, agent, generation, command }) {
361
- const backend = requireBackend();
362
- safeIdentifier(agent, "current peer");
381
+ function workerExecutable(command) {
363
382
  if (!Array.isArray(command) || !path.isAbsolute(command[0] ?? "")) {
364
383
  throw new Error("peer_recovery_absolute_worker_required");
365
384
  }
366
385
  const executable = realpathSync(command[0]);
367
386
  if (!lstatSync(executable).isFile()) throw new Error("peer_recovery_worker_invalid");
387
+ accessSync(executable, constants.X_OK);
388
+ return executable;
389
+ }
390
+
391
+ function existingHandoff(task, state, agent, generation) {
392
+ if (state.owner !== agent || state.generation !== generation || state.phase === "ready") return null;
393
+ const result = projection(task, state);
394
+ return state.phase === "exited" ? { ...result, exitCode: 2 } : result;
395
+ }
396
+
397
+ export async function runPeerTask({ workspaceRoot, taskId, agent, generation, command, handoff = false }) {
398
+ const backend = requireBackend();
399
+ safeIdentifier(agent, "current peer");
400
+ const executable = workerExecutable(command);
368
401
  const task = taskDirectory(workspaceRoot, taskId);
369
402
  const state = current(task);
370
403
  if (state.phase !== "ready" || state.owner !== agent || state.generation !== generation) {
404
+ const prior = handoff && existingHandoff(task, state, agent, generation);
405
+ if (prior) return prior;
371
406
  throw new Error("peer_recovery_attempt_not_available");
372
407
  }
373
408
  const id = randomUUID();
374
409
  const directory = mkdtempSync(path.join(realpathSync(tmpdir()), "owlrunkit-peer-"));
375
410
  materialize(checkpoint(task, state).files, directory);
376
- const attempt = { id, directory, backend: backend.id };
411
+ const attempt = { id, directory, backend: backend.id,
412
+ ...(backend.digest ? { backendSha256: backend.digest } : {}) };
377
413
  const claimed = append(task, state, {
378
414
  attempt, phase: "running", processExit: null, event: "peer_attempt_started",
379
415
  });
380
- if (!claimed) throw new Error("peer_recovery_attempt_already_claimed");
416
+ if (!claimed) {
417
+ const prior = handoff && existingHandoff(task, current(task), agent, generation);
418
+ if (prior) return prior;
419
+ throw new Error("peer_recovery_attempt_already_claimed");
420
+ }
381
421
 
382
422
  return new Promise((resolve, reject) => {
383
- const child = spawn(backend.executable, ["-p", sandboxPolicy(executable, directory),
384
- executable, ...command.slice(1)], {
423
+ const args = backend.id === "linux_landlock_seccomp_v1"
424
+ ? ["run", String(process.pid), directory]
425
+ : ["-p", sandboxPolicy(executable, directory)];
426
+ const child = spawn(backend.executable, [...args, executable, ...command.slice(1)], {
385
427
  cwd: directory, env: {
386
428
  PATH: "/usr/bin:/bin", TMPDIR: directory, OWLRUNKIT_PEER_ATTEMPT: id,
387
429
  OWLRUNKIT_PEER_PATHS: JSON.stringify(state.paths),
430
+ // An isolated worker cannot read host OpenSSL configuration.
431
+ ...(backend.id === "linux_landlock_seccomp_v1" ? { OPENSSL_CONF: "/dev/null" } : {}),
388
432
  }, stdio: ["pipe", "pipe", "pipe"],
389
433
  });
390
434
  let watcher, pending = Buffer.alloc(0), sentWake, protocolError = null;
@@ -440,3 +484,12 @@ export async function runPeerTask({ workspaceRoot, taskId, agent, generation, co
440
484
  } catch (error) { stop(error); }
441
485
  });
442
486
  }
487
+
488
+ export async function recoverAndRunPeerTask(options, command) {
489
+ workerExecutable(command);
490
+ const recovered = await recoverPeerTask(options);
491
+ if (recovered.generation !== options.generation + 1 || recovered.owner !== options.agent) {
492
+ return recovered;
493
+ }
494
+ return runPeerTask({ ...options, generation: recovered.generation, command, handoff: true });
495
+ }
@@ -27,10 +27,14 @@ const SELF_NORMALIZED_QUICK_CORE_VERSIONS = new Set([
27
27
  "0.24.0",
28
28
  "0.24.1",
29
29
  "0.25.0",
30
+ "0.25.1",
30
31
  ]);
31
32
  const EXACT_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
32
33
  const SHA256_REF = /^sha256:[a-f0-9]{64}$/;
33
34
  const TRUSTED_PRIOR_CORE_IDENTITIES = new Map([
35
+ ["0.25.0", new Set([
36
+ "sha256:20e75c386f1408f11e908a6d3ee1a39316d9728d5719fa06f8d6a5aa45abd83e",
37
+ ])],
34
38
  ["0.24.1", new Set([
35
39
  "sha256:94aaa2e3a6d504374fa78cfcd341e31b33087806c9ddf5f84b3d3b47a6328e08",
36
40
  ])],