ace-runtime 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/docs/usage.md +55 -0
- data/lib/ace/runtime/molecules/codex_runtime_installation.rb +130 -0
- data/lib/ace/runtime/molecules/execution_boot_baseline.rb +134 -0
- data/lib/ace/runtime/molecules/execution_network_selection.rb +85 -0
- data/lib/ace/runtime/molecules/execution_unit_installation.rb +375 -21
- data/lib/ace/runtime/molecules/kernel_view_topology.rb +133 -0
- data/lib/ace/runtime/molecules/owned_child_scope.rb +81 -0
- data/lib/ace/runtime/molecules/process_identity.rb +28 -0
- data/lib/ace/runtime/molecules/protected_artifact_set.rb +74 -9
- data/lib/ace/runtime/molecules/protected_socket.rb +24 -2
- data/lib/ace/runtime/molecules/protected_task_context_entry.rb +162 -0
- data/lib/ace/runtime/molecules/protected_worker_entry.rb +32 -0
- data/lib/ace/runtime/molecules/readiness_configuration.rb +154 -0
- data/lib/ace/runtime/molecules/readiness_runtime.rb +94 -0
- data/lib/ace/runtime/molecules/server_resource_observation.rb +183 -0
- data/lib/ace/runtime/molecules/systemd_scope_manager.rb +103 -15
- data/lib/ace/runtime/version.rb +1 -1
- data/lib/ace/runtime.rb +1 -0
- data/native/README.md +61 -6
- data/native/build-worker-gate +4 -2
- data/native/worker_gate.c +206 -20
- metadata +12 -2
data/native/README.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
The `ace-runtime` gem ships `native/worker_gate.c` and `native/build-worker-gate`.
|
|
4
4
|
This is a Linux executable, not a Ruby interpreter wrapper. Build on the target
|
|
5
|
-
architecture from the installed gem with the system compiler
|
|
5
|
+
architecture from the installed gem with the system compiler, json-c parser and OpenSSL 3 libcrypto.
|
|
6
6
|
Supported initial architectures are Linux x86_64 and aarch64 with little-endian
|
|
7
7
|
ELF64. A bootstrap built on another architecture fails deployment preflight.
|
|
8
8
|
|
|
9
9
|
On Debian bookworm install the distribution's `build-essential`, `pkg-config`
|
|
10
|
-
|
|
11
|
-
`libjson-c5`, libc and ELF loader at runtime. Use the same distribution/architecture
|
|
10
|
+
`libjson-c-dev` and `libssl-dev` build packages. The executable requires the matching system
|
|
11
|
+
`libjson-c5`, OpenSSL 3 `libcrypto.so.3` (`libssl3`), libc and ELF loader at runtime. Use the same distribution/architecture
|
|
12
12
|
for build and deployment; retain package versions, source SHA, compiler version,
|
|
13
13
|
ELF machine, `readelf -d` dependencies and output SHA256 in installer evidence.
|
|
14
14
|
These libraries and their loader search directories must remain root-owned and
|
|
@@ -33,8 +33,9 @@ All installation ancestors are root-owned and non-worker-writable. The fixed
|
|
|
33
33
|
root-owned `/etc/ace/assignment-authorities.json` records `bootstrap` and its exact
|
|
34
34
|
`bootstrap_sha256`. ACE verifies digest, ELF architecture, executable permissions,
|
|
35
35
|
ownership and ancestry before reservation. The native layout uses only this
|
|
36
|
-
absolute executable, installed mapping ID and correlation ticket;
|
|
37
|
-
|
|
36
|
+
absolute executable, installed mapping ID and correlation ticket; cwd and environment come from the root-controlled mapping after authority release.
|
|
37
|
+
The mandatory closed `worker_entry` pins interpreter and wrapper references; arbitrary
|
|
38
|
+
worker argv and shebang/PATH execution are not admitted.
|
|
38
39
|
The map grants no UID switching and the binary is never privileged.
|
|
39
40
|
|
|
40
41
|
The root-installed authority, launcher and native server units require:
|
|
@@ -55,15 +56,69 @@ mandatory; an empty capability set alone does not stop same-UID injection.
|
|
|
55
56
|
|
|
56
57
|
The bootstrap sets non-dumpable before connecting, requires enforced Yama
|
|
57
58
|
`ptrace_scope=2`, requires empty Inh/Prm/Eff/Bnd/Amb capability sets and NoNewPrivs=1, verifies
|
|
58
|
-
worker IDs/groups and
|
|
59
|
+
worker IDs/groups and its retained actual kernel parent birth, authenticates authority
|
|
59
60
|
SO_PEERCRED and protected socket ancestry, and waits at most 30 seconds on one
|
|
60
61
|
stream. It never reconnects or executes after pre-release EOF, malformed input
|
|
61
62
|
or timeout. A committed release whose response is lost remains potentially
|
|
62
63
|
executed; public inspection must retain ownership rather than resend release.
|
|
63
64
|
|
|
65
|
+
The gate accepts only the maintained `ace.assign.authorities/v2` mapping. It
|
|
66
|
+
captures its actual parent PID and kernel boot/start-tick birth before reading
|
|
67
|
+
the map, checks that same birth before and after parent credentials, and repeats
|
|
68
|
+
the check before authority admission and immediately before exec. The canonical
|
|
69
|
+
`gate_ready` authority owner associates the actual gate peer and its parent with
|
|
70
|
+
the original recorded child/server; the map supplies no `server_identity`.
|
|
71
|
+
Changed parent birth, reparenting, inaccessible credentials or changed policy
|
|
72
|
+
refuses. The original retained mapping supplies `worker_entry={interpreter:REF,wrapper:REF}`
|
|
73
|
+
in the authenticated release, rather than selecting code from the current map.
|
|
74
|
+
Interpreter references are bounded to 32MiB and wrapper references to 1MiB.
|
|
75
|
+
Both root-protected regular held files are hashed and their metadata rechecked.
|
|
76
|
+
The interpreter must be an executable native ELF. The verified wrapper is copied
|
|
77
|
+
into a memfd sealed against writes/growth/shrinkage, reopened read-only, and the
|
|
78
|
+
writable descriptor dropped. Selected held interpreter FD4 and code FD5 survive
|
|
79
|
+
`fexecve(4, ["/proc/self/fd/4", "-I", "-S", "-B", "/proc/self/fd/5", "authority", "worker"], env)`.
|
|
80
|
+
Inherited stdin/stdout/stderr remain untouched; no provider lifetime timeout is added.
|
|
81
|
+
The retained original source owner inside the wrapper separately validates and
|
|
82
|
+
executes the accepted Ruby snapshot and its package closure.
|
|
83
|
+
|
|
84
|
+
Release is one compact JSON record containing the original ticket/assignment/
|
|
85
|
+
attempt/generation/commit plus `prepared_input`. That closed object pins the
|
|
86
|
+
original registration generation/commit, definition and original binding digest,
|
|
87
|
+
prepared-work reference, original worker entry pair and exact journal bundle reference/length/SHA256. The
|
|
88
|
+
authority verifies the original bundle before durable issue; subsequent prepared
|
|
89
|
+
fetch authenticates the accepted release pin before returning body bytes. The
|
|
90
|
+
gate validates the closed ten-field pin, including the original workspace exclusion,
|
|
91
|
+
digests and unchanged bounds before exec. The exclusion has five closed fields:
|
|
92
|
+
key, authority UID/GID, readonly root resource and original cwd resource. Each
|
|
93
|
+
resource has the existing eight identity fields. The gate checks integer/path/
|
|
94
|
+
filesystem bounds and exact mapping/view/key association; the retained Ruby owner
|
|
95
|
+
verifies the canonical cwd digest and held filesystem identity before acquiring
|
|
96
|
+
the lifetime reader. This metadata does not itself grant filesystem access.
|
|
97
|
+
The complete encoded release is bounded to16KiB before durable issue and at the gate. It
|
|
98
|
+
does not receive inline work or propagate an input FD to providers. The worker
|
|
99
|
+
uses the existing authenticated original prepared fetch after release.
|
|
100
|
+
|
|
64
101
|
An ordinary Docker kernel with Yama=0 can verify compilation and fail-closed
|
|
65
102
|
policy rejection. It cannot demonstrate protected launch acceptance. A real
|
|
66
103
|
isolated Linux deployment enforcing the complete policy is required for that
|
|
67
104
|
positive proof; installing this gem does not change host sysctls or install OS
|
|
68
105
|
service/account policy. Downstream gad.8 provisions those requirements and
|
|
69
106
|
consumes this shipped build mechanism.
|
|
107
|
+
|
|
108
|
+
## Fixed digest initialization and acceptance limits
|
|
109
|
+
|
|
110
|
+
Before any OpenSSL API call the gate selects `OPENSSL_INIT_NO_LOAD_CONFIG`.
|
|
111
|
+
It creates a private library context, fixes its provider search path to `/dev/null`
|
|
112
|
+
(a non-directory), explicitly loads only `default`, and fetches `SHA256` with
|
|
113
|
+
`provider=default`. Failure refuses execution; no config/engine/module fallback
|
|
114
|
+
is provided. [Initialization semantics](https://docs.openssl.org/3.0/man3/OPENSSL_init_crypto/),
|
|
115
|
+
[provider search path](https://docs.openssl.org/3.0/man3/OSSL_PROVIDER/), and
|
|
116
|
+
[fixed provider property](https://docs.openssl.org/3.0/man7/OSSL_PROVIDER-default/) and
|
|
117
|
+
[libcrypto built-in default provider](https://docs.openssl.org/3.0/man7/crypto/#default-provider)
|
|
118
|
+
are the maintained API basis. The system libcrypto/loader remain in the accepted
|
|
119
|
+
root-controlled OS artifact closure; this does not attest an arbitrary library build.
|
|
120
|
+
|
|
121
|
+
This source checkpoint does not compile or execute native code. Real ELF,
|
|
122
|
+
OpenSSL linkage/provider availability, memfd seals, retained FD execution and
|
|
123
|
+
Python script/stdlib behavior require installed Linux acceptance. The trusted
|
|
124
|
+
provisioned OS/Python premise remains; no separate Python runtime tree is introduced.
|
data/native/build-worker-gate
CHANGED
|
@@ -8,8 +8,10 @@ case "$1" in /*) ;; *) echo 'output must be absolute' >&2; exit 2;; esac
|
|
|
8
8
|
source_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
9
9
|
build_dir=$(mktemp -d)
|
|
10
10
|
trap 'rm -rf -- "$build_dir"' EXIT HUP INT TERM
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
env -i PATH=/usr/bin:/bin /usr/bin/pkg-config --atleast-version=3.0.0 libcrypto
|
|
12
|
+
env -i PATH=/usr/bin:/bin /usr/bin/pkg-config --atleast-version=0.15 json-c
|
|
13
|
+
cflags=$(env -i PATH=/usr/bin:/bin /usr/bin/pkg-config --cflags json-c libcrypto)
|
|
14
|
+
libs=$(env -i PATH=/usr/bin:/bin /usr/bin/pkg-config --libs json-c libcrypto)
|
|
13
15
|
# pkg-config values come only from the installed system development package.
|
|
14
16
|
# shellcheck disable=SC2086
|
|
15
17
|
env -i PATH=/usr/bin:/bin /usr/bin/cc -std=c11 -O2 -D_FORTIFY_SOURCE=3 \
|
data/native/worker_gate.c
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
#include <fcntl.h>
|
|
4
4
|
#include <json-c/json.h>
|
|
5
5
|
#include <poll.h>
|
|
6
|
+
#include <openssl/crypto.h>
|
|
7
|
+
#include <openssl/evp.h>
|
|
8
|
+
#include <openssl/provider.h>
|
|
9
|
+
#include <sys/mman.h>
|
|
6
10
|
#include <stdio.h>
|
|
11
|
+
#include <stdint.h>
|
|
7
12
|
#include <stdlib.h>
|
|
8
13
|
#include <string.h>
|
|
9
14
|
#include <sys/prctl.h>
|
|
@@ -20,6 +25,7 @@ static _Noreturn void deny(const char *why) { fprintf(stderr, "ace-worker-gate:
|
|
|
20
25
|
static double now(void) { struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC,&ts)) deny("clock unavailable"); return ts.tv_sec + ts.tv_nsec / 1e9; }
|
|
21
26
|
static int token(const char *s) {
|
|
22
27
|
if (!s || !*s || strlen(s)>128) return 0;
|
|
28
|
+
if(!((s[0]>='a'&&s[0]<='z')||(s[0]>='A'&&s[0]<='Z')||(s[0]>='0'&&s[0]<='9')))return 0;
|
|
23
29
|
for (const unsigned char *p=(const unsigned char *)s; *p; p++)
|
|
24
30
|
if (!((*p>='a'&&*p<='z')||(*p>='A'&&*p<='Z')||(*p>='0'&&*p<='9')||*p=='_'||*p=='-'||*p=='.')) return 0;
|
|
25
31
|
return 1;
|
|
@@ -29,14 +35,18 @@ static json_object *field(json_object *o,const char *k,enum json_type t) {
|
|
|
29
35
|
if (!o || !json_object_object_get_ex(o,k,&v) || !json_object_is_type(v,t)) deny("invalid protected schema");
|
|
30
36
|
return v;
|
|
31
37
|
}
|
|
32
|
-
static const char *str(json_object *o,const char *k) {
|
|
38
|
+
static const char *str(json_object *o,const char *k) {
|
|
39
|
+
json_object *value=field(o,k,json_type_string);const char *text=json_object_get_string(value);
|
|
40
|
+
if((size_t)json_object_get_string_len(value)!=strlen(text))deny("embedded NUL in protected string");
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
33
43
|
static int number(json_object *o,const char *k) {
|
|
34
44
|
int64_t n=json_object_get_int64(field(o,k,json_type_int));
|
|
35
45
|
if (n<=0 || n>2147483647) deny("invalid principal identity");
|
|
36
46
|
return (int)n;
|
|
37
47
|
}
|
|
38
48
|
static void trusted_path(const char *path,int dir,uid_t owner) {
|
|
39
|
-
char copy[
|
|
49
|
+
char copy[4097]; size_t n=strlen(path); struct stat st;
|
|
40
50
|
if (!n || n>=sizeof(copy) || path[0]!='/' || strstr(path,"//") || strstr(path,"/../") || strstr(path,"/./") ||
|
|
41
51
|
!strcmp(path+n-1,"/") || !strcmp(path+n-(n>=3?3:n),"/..")) deny("invalid protected path");
|
|
42
52
|
memcpy(copy,path,n+1);
|
|
@@ -87,19 +97,179 @@ static void credentials(pid_t pid,int uid,int gid,json_object *groups) {
|
|
|
87
97
|
}
|
|
88
98
|
free(s); if(!got_uid||!got_gid||!got_groups||!no_new_privs||caps!=31)deny("kernel credentials unavailable");
|
|
89
99
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if(getppid()!=pid) deny("gate
|
|
100
|
+
struct parent_identity { pid_t pid; char birth[256]; };
|
|
101
|
+
static void parent_birth(pid_t pid,char birth[256]) {
|
|
102
|
+
if(pid<=1||getppid()!=pid) deny("original gate parent unavailable");
|
|
93
103
|
char path[64]; snprintf(path,sizeof(path),"/proc/%d/stat",pid); char *s=read_file(path,16384);
|
|
94
104
|
char *end=strrchr(s,')'); if(!end||end[1]!=' ') deny("server birth unavailable");
|
|
95
105
|
char *save=NULL,*word=strtok_r(end+2," ", &save); int i=3; const char *ticks=NULL;
|
|
96
106
|
while(word) { if(i==22){ticks=word;break;} i++;word=strtok_r(NULL," ",&save); }
|
|
97
107
|
if(!ticks||strspn(ticks,"0123456789")!=strlen(ticks))deny("server birth unavailable");
|
|
98
108
|
char *boot=read_file("/proc/sys/kernel/random/boot_id",128); boot[strcspn(boot,"\n")]=0;
|
|
99
|
-
|
|
100
|
-
if(
|
|
109
|
+
int written=snprintf(birth,256,"linux:%s:%s",boot,ticks);
|
|
110
|
+
if(written<0||written>=256)deny("parent birth oversized");
|
|
101
111
|
free(boot);free(s);
|
|
102
|
-
|
|
112
|
+
if(getppid()!=pid)deny("original gate parent changed");
|
|
113
|
+
}
|
|
114
|
+
static void same_parent(const struct parent_identity *parent) {
|
|
115
|
+
char birth[256];parent_birth(parent->pid,birth);
|
|
116
|
+
if(strcmp(birth,parent->birth))deny("original gate parent incarnation changed");
|
|
117
|
+
}
|
|
118
|
+
static void parent_credentials(const struct parent_identity *parent,json_object *mapping) {
|
|
119
|
+
same_parent(parent);
|
|
120
|
+
credentials(parent->pid,number(mapping,"worker_uid"),number(mapping,"worker_gid"),field(mapping,"worker_groups",json_type_array));
|
|
121
|
+
same_parent(parent);
|
|
122
|
+
}
|
|
123
|
+
static void closed(json_object *object,const char *const *keys,size_t count) {
|
|
124
|
+
if(!json_object_is_type(object,json_type_object)||(size_t)json_object_object_length(object)!=count)deny("open protected schema");
|
|
125
|
+
for(size_t i=0;i<count;i++) { json_object *value=NULL;if(!json_object_object_get_ex(object,keys[i],&value))deny("incomplete protected schema"); }
|
|
126
|
+
}
|
|
127
|
+
static void hex_field(json_object *object,const char *key,size_t min,size_t max) {
|
|
128
|
+
const char *value=str(object,key);size_t length=(size_t)json_object_get_string_len(field(object,key,json_type_string));
|
|
129
|
+
if(length!=strlen(value)||(length!=min&&length!=max)||strspn(value,"0123456789abcdef")!=length)deny("invalid protected digest");
|
|
130
|
+
}
|
|
131
|
+
static void bounded_integer(json_object *object,const char *key,int64_t max) {
|
|
132
|
+
int64_t value=json_object_get_int64(field(object,key,json_type_int));
|
|
133
|
+
if(value<1||value>max)deny("invalid prepared bound");
|
|
134
|
+
}
|
|
135
|
+
static void entry_reference(json_object *reference,int64_t limit) {
|
|
136
|
+
static const char *const keys[]={"path","bytes","sha256"};
|
|
137
|
+
closed(reference,keys,3);bounded_integer(reference,"bytes",limit);hex_field(reference,"sha256",64,64);
|
|
138
|
+
const char *path=str(reference,"path");size_t n=strlen(path);
|
|
139
|
+
if(n<2||n>4096||path[0]!='/'||path[n-1]=='/'||strstr(path,"//")||strstr(path,"/./")||strstr(path,"/../")||
|
|
140
|
+
!strcmp(path+n-2,"/.")||(n>=3&&!strcmp(path+n-3,"/..")))deny("invalid worker reference path");
|
|
141
|
+
}
|
|
142
|
+
static void worker_entry(json_object *entry) {
|
|
143
|
+
static const char *const keys[]={"interpreter","wrapper"};closed(entry,keys,2);
|
|
144
|
+
entry_reference(field(entry,"interpreter",json_type_object),33554432);
|
|
145
|
+
entry_reference(field(entry,"wrapper",json_type_object),1048576);
|
|
146
|
+
}
|
|
147
|
+
static void workspace_resource(json_object *resource) {
|
|
148
|
+
static const char *const keys[]={"device","filesystem_type","gid","host_path","inode","mount_id","uid","view_path"};
|
|
149
|
+
closed(resource,keys,8);
|
|
150
|
+
const char *numbers[]={"device","inode","uid","gid"};
|
|
151
|
+
for(size_t i=0;i<4;i++)if(json_object_get_int64(field(resource,numbers[i],json_type_int))<0)deny("invalid workspace identity");
|
|
152
|
+
bounded_integer(resource,"mount_id",INT64_MAX);
|
|
153
|
+
const char *fs=str(resource,"filesystem_type");
|
|
154
|
+
if(strcmp(fs,"ext4")&&strcmp(fs,"xfs")&&strcmp(fs,"btrfs")&&strcmp(fs,"tmpfs"))deny("invalid workspace filesystem");
|
|
155
|
+
const char *paths[]={"host_path","view_path"};
|
|
156
|
+
for(size_t i=0;i<2;i++) {
|
|
157
|
+
const char *path=str(resource,paths[i]);size_t n=strlen(path);
|
|
158
|
+
if(!n||n>4096||path[0]!='/'||(n>1&&path[n-1]=='/')||strstr(path,"//")||strstr(path,"/./")||strstr(path,"/../")||
|
|
159
|
+
(n>=2&&!strcmp(path+n-2,"/."))||(n>=3&&!strcmp(path+n-3,"/..")))deny("invalid workspace path");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
static void workspace_exclusion(json_object *projection,const char *mapping_id) {
|
|
163
|
+
static const char *const keys[]={"authority_gid","authority_uid","key","root_resource","worker_cwd_resource"};
|
|
164
|
+
closed(projection,keys,5);number(projection,"authority_uid");number(projection,"authority_gid");
|
|
165
|
+
json_object *root=field(projection,"root_resource",json_type_object);
|
|
166
|
+
workspace_resource(root);workspace_resource(field(projection,"worker_cwd_resource",json_type_object));
|
|
167
|
+
char view[256],prefix[256];
|
|
168
|
+
int v=snprintf(view,sizeof(view),"/run/ace/lifecycle-exclusion/%s",mapping_id);
|
|
169
|
+
int p=snprintf(prefix,sizeof(prefix),"workspace:%s:",mapping_id);
|
|
170
|
+
const char *key=str(projection,"key");size_t n=strlen(key);
|
|
171
|
+
if(v<0||(size_t)v>=sizeof(view)||p<0||(size_t)p>=sizeof(prefix)||strcmp(view,str(root,"view_path"))||
|
|
172
|
+
n!=(size_t)p+64||strncmp(key,prefix,(size_t)p))deny("workspace mapping differs");
|
|
173
|
+
for(size_t i=(size_t)p;i<n;i++)if(!((key[i]>='0'&&key[i]<='9')||(key[i]>='a'&&key[i]<='f')))deny("invalid workspace digest");
|
|
174
|
+
char suffix[256];int s=snprintf(suffix,sizeof(suffix),"/workspaces/%s/%s",mapping_id,key+p);
|
|
175
|
+
const char *host=str(root,"host_path");size_t h=strlen(host);
|
|
176
|
+
if(s<0||(size_t)s>=sizeof(suffix)||h<(size_t)s||strcmp(host+h-s,suffix)||
|
|
177
|
+
json_object_get_int64(field(root,"uid",json_type_int))!=number(projection,"authority_uid")||
|
|
178
|
+
json_object_get_int64(field(root,"gid",json_type_int))!=number(projection,"authority_gid"))deny("workspace owner differs");
|
|
179
|
+
/* The original Ruby owner verifies the canonical cwd digest and held inode.
|
|
180
|
+
These closed release fields convey no independent filesystem grant. */
|
|
181
|
+
}
|
|
182
|
+
static void prepared_permission(json_object *permission,const char *mapping_id) {
|
|
183
|
+
static const char *const permission_keys[]={"operation","launch_ticket","attempt_id","assignment_id","generation","journal_commit","prepared_input"};
|
|
184
|
+
closed(permission,permission_keys,7);hex_field(permission,"journal_commit",40,64);
|
|
185
|
+
json_object *input=field(permission,"prepared_input",json_type_object);
|
|
186
|
+
static const char *const input_keys[]={"registration_generation","registration_commit","definition_digest","original_binding_digest","prepared_work","bundle_ref","bundle_bytes","bundle_sha256","worker_entry","workspace_exclusion"};
|
|
187
|
+
closed(input,input_keys,10);worker_entry(field(input,"worker_entry",json_type_object));bounded_integer(input,"registration_generation",INT64_MAX);
|
|
188
|
+
workspace_exclusion(field(input,"workspace_exclusion",json_type_object),mapping_id);
|
|
189
|
+
hex_field(input,"registration_commit",40,64);hex_field(input,"definition_digest",64,64);
|
|
190
|
+
hex_field(input,"original_binding_digest",64,64);hex_field(input,"bundle_sha256",64,64);
|
|
191
|
+
bounded_integer(input,"bundle_bytes",64*1024*1024);
|
|
192
|
+
char expected[256];
|
|
193
|
+
int written=snprintf(expected,sizeof(expected),"execution/prepared/%s-%s.bundle",str(permission,"assignment_id"),str(input,"bundle_sha256"));
|
|
194
|
+
if(written<0||(size_t)written>=sizeof(expected)||
|
|
195
|
+
strcmp(expected,str(input,"bundle_ref"))||json_object_get_string_len(field(input,"bundle_ref",json_type_string))!=(int)strlen(expected))deny("prepared bundle reference differs");
|
|
196
|
+
json_object *work=field(input,"prepared_work",json_type_object);
|
|
197
|
+
static const char *const work_keys[]={"version","task_id","scope","prepared_head","prepared_tree","manifest_bytes","manifest_sha256","selection_sha256"};
|
|
198
|
+
closed(work,work_keys,8);
|
|
199
|
+
const char *task=str(work,"task_id");size_t task_length=(size_t)json_object_get_string_len(field(work,"task_id",json_type_string));
|
|
200
|
+
if(json_object_get_int64(field(work,"version",json_type_int))!=1||task_length!=strlen(task)||!task_length||task_length>200||
|
|
201
|
+
!((task[0]>='a'&&task[0]<='z')||(task[0]>='A'&&task[0]<='Z')||(task[0]>='0'&&task[0]<='9')))deny("invalid prepared selection");
|
|
202
|
+
for(size_t i=0;i<task_length;i++)if(!((task[i]>='a'&&task[i]<='z')||(task[i]>='A'&&task[i]<='Z')||(task[i]>='0'&&task[i]<='9')||task[i]=='_'||task[i]=='-'||task[i]=='.'))deny("invalid prepared task");
|
|
203
|
+
const char *scope=str(work,"scope");size_t scope_length=(size_t)json_object_get_string_len(field(work,"scope",json_type_string));
|
|
204
|
+
if(scope_length!=strlen(scope)||!scope_length)deny("invalid prepared scope");
|
|
205
|
+
size_t digits=0;
|
|
206
|
+
for(size_t i=0;i<scope_length;i++) {
|
|
207
|
+
if(scope[i]>='0'&&scope[i]<='9')digits++;
|
|
208
|
+
else if(scope[i]=='.'&&digits>0)digits=0;
|
|
209
|
+
else deny("invalid prepared scope");
|
|
210
|
+
}
|
|
211
|
+
if(!digits)deny("invalid prepared scope");
|
|
212
|
+
hex_field(work,"prepared_head",40,64);hex_field(work,"prepared_tree",40,64);
|
|
213
|
+
hex_field(work,"manifest_sha256",64,64);hex_field(work,"selection_sha256",64,64);
|
|
214
|
+
bounded_integer(work,"manifest_bytes",32768);
|
|
215
|
+
}
|
|
216
|
+
/* First OpenSSL call suppresses config; a private context has only the fixed
|
|
217
|
+
default provider. /dev/null is deliberately not a module directory. */
|
|
218
|
+
static EVP_MD *fixed_sha256(OSSL_LIB_CTX **context,OSSL_PROVIDER **provider) {
|
|
219
|
+
if(!OPENSSL_init_crypto(OPENSSL_INIT_NO_LOAD_CONFIG,NULL))deny("digest initialization failed");
|
|
220
|
+
*context=OSSL_LIB_CTX_new();if(!*context||!OSSL_PROVIDER_set_default_search_path(*context,"/dev/null"))deny("digest context failed");
|
|
221
|
+
*provider=OSSL_PROVIDER_load(*context,"default");if(!*provider)deny("fixed digest provider unavailable");
|
|
222
|
+
EVP_MD *digest=EVP_MD_fetch(*context,"SHA256","provider=default");if(!digest)deny("fixed digest unavailable");return digest;
|
|
223
|
+
}
|
|
224
|
+
static int same_metadata(const struct stat *a,const struct stat *b) {
|
|
225
|
+
return a->st_dev==b->st_dev&&a->st_ino==b->st_ino&&a->st_uid==b->st_uid&&a->st_gid==b->st_gid&&
|
|
226
|
+
a->st_mode==b->st_mode&&a->st_size==b->st_size&&a->st_mtim.tv_sec==b->st_mtim.tv_sec&&
|
|
227
|
+
a->st_mtim.tv_nsec==b->st_mtim.tv_nsec&&a->st_ctim.tv_sec==b->st_ctim.tv_sec&&a->st_ctim.tv_nsec==b->st_ctim.tv_nsec;
|
|
228
|
+
}
|
|
229
|
+
static int held_entry(json_object *reference,int executable,const EVP_MD *digest,struct stat *verified) {
|
|
230
|
+
const char *path=str(reference,"path");root_path(path,0);
|
|
231
|
+
int fd=open(path,O_RDONLY|O_NONBLOCK|O_NOFOLLOW|O_CLOEXEC);if(fd<0)deny("worker entry unavailable");
|
|
232
|
+
struct stat before,after;
|
|
233
|
+
int64_t expected=json_object_get_int64(field(reference,"bytes",json_type_int));
|
|
234
|
+
if(fstat(fd,&before)||!S_ISREG(before.st_mode)||before.st_uid!=0||(before.st_mode&06022)||before.st_size!=expected||
|
|
235
|
+
(executable&&!(before.st_mode&0111)))deny("unsafe held worker entry");
|
|
236
|
+
unsigned char magic[4];
|
|
237
|
+
if(executable&&(pread(fd,magic,sizeof(magic),0)!=(ssize_t)sizeof(magic)||memcmp(magic,"\177ELF",4)))deny("worker interpreter is not native ELF");
|
|
238
|
+
int snapshot=-1;if(!executable) {snapshot=memfd_create("ace-worker-source",MFD_CLOEXEC|MFD_ALLOW_SEALING);if(snapshot<0)deny("worker snapshot unavailable");}
|
|
239
|
+
EVP_MD_CTX *ctx=EVP_MD_CTX_new();if(!ctx||!EVP_DigestInit_ex(ctx,digest,NULL))deny("worker digest unavailable");
|
|
240
|
+
unsigned char buffer[65536],sum[EVP_MAX_MD_SIZE];size_t total=0;unsigned int sum_size=0;
|
|
241
|
+
for(;;) {
|
|
242
|
+
ssize_t n=read(fd,buffer,sizeof(buffer));if(n<0&&errno==EINTR)continue;if(n<0)deny("held worker read failed");if(!n)break;
|
|
243
|
+
if((int64_t)total+n>expected)deny("held worker entry enlarged");
|
|
244
|
+
if(!EVP_DigestUpdate(ctx,buffer,(size_t)n))deny("worker digest failed");
|
|
245
|
+
total+=(size_t)n;
|
|
246
|
+
if(snapshot>=0) {size_t offset=0;while(offset<(size_t)n) {ssize_t written=write(snapshot,buffer+offset,(size_t)n-offset);
|
|
247
|
+
if(written<0&&errno==EINTR)continue;
|
|
248
|
+
if(written<=0)deny("worker snapshot write failed");
|
|
249
|
+
offset+=(size_t)written;}}
|
|
250
|
+
}
|
|
251
|
+
if(total!=(size_t)expected||!EVP_DigestFinal_ex(ctx,sum,&sum_size)||sum_size!=32||fstat(fd,&after)||!same_metadata(&before,&after))deny("held worker entry changed");
|
|
252
|
+
EVP_MD_CTX_free(ctx);char hex[65];for(unsigned int i=0;i<32;i++)snprintf(hex+i*2,3,"%02x",sum[i]);
|
|
253
|
+
if(strcmp(hex,str(reference,"sha256")))deny("held worker digest differs");
|
|
254
|
+
*verified=after;
|
|
255
|
+
if(executable) {if(lseek(fd,0,SEEK_SET)<0)deny("worker interpreter seek failed");return fd;}
|
|
256
|
+
close(fd);
|
|
257
|
+
int seals=F_SEAL_WRITE|F_SEAL_GROW|F_SEAL_SHRINK|F_SEAL_SEAL;
|
|
258
|
+
if(fcntl(snapshot,F_ADD_SEALS,seals)||fcntl(snapshot,F_GET_SEALS)!=seals)deny("worker snapshot sealing failed");
|
|
259
|
+
char descriptor[64];snprintf(descriptor,sizeof(descriptor),"/proc/self/fd/%d",snapshot);
|
|
260
|
+
int readonly=open(descriptor,O_RDONLY|O_CLOEXEC);if(readonly<0)deny("readonly worker snapshot unavailable");
|
|
261
|
+
struct stat sealed,reopened;
|
|
262
|
+
if(fstat(snapshot,&sealed)||fstat(readonly,&reopened)||sealed.st_dev!=reopened.st_dev||sealed.st_ino!=reopened.st_ino||
|
|
263
|
+
reopened.st_size!=expected||fcntl(readonly,F_GET_SEALS)!=seals)deny("worker snapshot identity differs");
|
|
264
|
+
*verified=reopened;
|
|
265
|
+
close(snapshot);return readonly;
|
|
266
|
+
}
|
|
267
|
+
static void select_entry_descriptors(int interpreter,int wrapper) {
|
|
268
|
+
int held_interpreter=fcntl(interpreter,F_DUPFD_CLOEXEC,6),held_wrapper=fcntl(wrapper,F_DUPFD_CLOEXEC,6);
|
|
269
|
+
if(held_interpreter<0||held_wrapper<0)deny("worker descriptor reservation failed");
|
|
270
|
+
close(interpreter);close(wrapper);
|
|
271
|
+
if(dup2(held_interpreter,4)!=4||dup2(held_wrapper,5)!=5||fcntl(4,F_SETFD,0)||fcntl(5,F_SETFD,0))deny("worker descriptor selection failed");
|
|
272
|
+
close(held_interpreter);close(held_wrapper);
|
|
103
273
|
}
|
|
104
274
|
static void wait_fd(int fd,short events,double deadline) {
|
|
105
275
|
struct pollfd p={.fd=fd,.events=events}; int r;
|
|
@@ -125,26 +295,32 @@ static json_object *receive(int fd,double deadline) {
|
|
|
125
295
|
while(n<LIMIT) { wait_fd(fd,POLLIN,deadline); ssize_t r=recv(fd,s+n,1,0);
|
|
126
296
|
if(r<0&&(errno==EINTR||errno==EAGAIN))continue;
|
|
127
297
|
if(r<=0)deny("authority EOF before permission");
|
|
128
|
-
if(s[n++]=='\n') { json_tokener *t=json_tokener_new();json_object *o=json_tokener_parse_ex(t,s,(int)n);
|
|
298
|
+
if(s[n++]=='\n') { json_tokener *t=json_tokener_new();json_tokener_set_flags(t,JSON_TOKENER_VALIDATE_UTF8);json_object *o=json_tokener_parse_ex(t,s,(int)n);
|
|
129
299
|
if(!o||json_tokener_get_error(t)!=json_tokener_success||json_tokener_get_parse_end(t)!=n||!json_object_is_type(o,json_type_object))deny("invalid authority frame");
|
|
300
|
+
/* The maintained wire emits one compact JSON record. Round-trip equality
|
|
301
|
+
refuses duplicate keys, extra whitespace/records and alternate escapes. */
|
|
302
|
+
const char *canonical=json_object_to_json_string_ext(o,JSON_C_TO_STRING_PLAIN|JSON_C_TO_STRING_NOSLASHESCAPE);
|
|
303
|
+
if(strlen(canonical)!=n-1||memcmp(canonical,s,n-1))deny("noncanonical authority frame");
|
|
130
304
|
json_tokener_free(t);free(s);return o; }
|
|
131
305
|
}
|
|
132
306
|
deny("authority frame oversized"); return NULL;
|
|
133
307
|
}
|
|
134
308
|
int main(int argc,char **argv) {
|
|
309
|
+
struct parent_identity original_parent={.pid=getppid()};
|
|
310
|
+
parent_birth(original_parent.pid,original_parent.birth);same_parent(&original_parent);
|
|
135
311
|
if(prctl(PR_SET_DUMPABLE,0,0,0,0))deny("non-dumpable gate unavailable");
|
|
136
312
|
if(argc!=3||!token(argv[1])||!token(argv[2]))deny("expected fixed mapping and correlation ticket");
|
|
137
313
|
policy();root_path(MAP_PATH,0);char *bytes=read_file(MAP_PATH,LIMIT);
|
|
138
|
-
json_tokener *parser=json_tokener_new();json_object *map=json_tokener_parse_ex(parser,bytes,(int)strlen(bytes));
|
|
314
|
+
json_tokener *parser=json_tokener_new();json_tokener_set_flags(parser,JSON_TOKENER_VALIDATE_UTF8);json_object *map=json_tokener_parse_ex(parser,bytes,(int)strlen(bytes));
|
|
139
315
|
if(!map||json_tokener_get_error(parser)!=json_tokener_success||json_tokener_get_parse_end(parser)!=strlen(bytes))deny("invalid deployment map");
|
|
140
316
|
json_tokener_free(parser);free(bytes);
|
|
141
|
-
if(strcmp(str(map,"schema"),"ace.assign.authorities/
|
|
317
|
+
if(strcmp(str(map,"schema"),"ace.assign.authorities/v2"))deny("unsupported deployment map");
|
|
142
318
|
json_object *mapping=field(field(map,"launch_mappings",json_type_object),argv[1],json_type_object);
|
|
143
319
|
json_object *service=field(field(map,"authorities",json_type_object),str(mapping,"authority_id"),json_type_object);
|
|
144
320
|
int worker=number(mapping,"worker_uid"),launcher=number(mapping,"launcher_uid"),authority=number(service,"uid");
|
|
145
321
|
if(worker==launcher||worker==authority||authority==launcher)deny("principals are not distinct");
|
|
146
322
|
credentials(getpid(),worker,number(mapping,"worker_gid"),field(mapping,"worker_groups",json_type_array));
|
|
147
|
-
|
|
323
|
+
parent_credentials(&original_parent,mapping);
|
|
148
324
|
const char *bootstrap=str(mapping,"bootstrap");root_path(bootstrap,0);
|
|
149
325
|
struct stat installed,self;
|
|
150
326
|
if(stat(bootstrap,&installed)||stat("/proc/self/exe",&self)||installed.st_dev!=self.st_dev||installed.st_ino!=self.st_ino||
|
|
@@ -171,7 +347,7 @@ int main(int argc,char **argv) {
|
|
|
171
347
|
json_object_object_add(request,"version",json_object_new_int(1));json_object_object_add(request,"operation",json_object_new_string("gate_ready"));
|
|
172
348
|
json_object_object_add(request,"mutation_id",NULL);json_object_object_add(request,"project_id",json_object_new_string(str(mapping,"project_id")));
|
|
173
349
|
json_object_object_add(params,"mapping_id",json_object_new_string(argv[1]));json_object_object_add(params,"launch_ticket",json_object_new_string(argv[2]));
|
|
174
|
-
json_object_object_add(request,"params",params);send_frame(fd,request,deadline);json_object_put(request);
|
|
350
|
+
json_object_object_add(request,"params",params);parent_credentials(&original_parent,mapping);send_frame(fd,request,deadline);json_object_put(request);
|
|
175
351
|
json_object *ready=receive(fd,deadline);
|
|
176
352
|
if(strcmp(str(ready,"status"),"ok")||strcmp(str(field(ready,"data",json_type_object),"phase"),"ready"))deny("gate not admitted");
|
|
177
353
|
json_object_put(ready);
|
|
@@ -179,12 +355,15 @@ int main(int argc,char **argv) {
|
|
|
179
355
|
if(strcmp(str(permission,"operation"),"release")||strcmp(str(permission,"launch_ticket"),argv[2])||
|
|
180
356
|
!token(str(permission,"attempt_id"))||!token(str(permission,"assignment_id"))||!token(str(permission,"journal_commit")))deny("release permission differs");
|
|
181
357
|
if(json_object_get_int64(field(permission,"generation",json_type_int))<1)deny("invalid release generation");
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
358
|
+
if(strlen(json_object_to_json_string_ext(permission,JSON_C_TO_STRING_PLAIN|JSON_C_TO_STRING_NOSLASHESCAPE))+1>16384)deny("release envelope oversized");
|
|
359
|
+
prepared_permission(permission,argv[1]);
|
|
360
|
+
policy();parent_credentials(&original_parent,mapping);credentials(peer.pid,authority,number(service,"gid"),field(service,"groups",json_type_array));
|
|
361
|
+
json_object *entry=field(field(permission,"prepared_input",json_type_object),"worker_entry",json_type_object);
|
|
362
|
+
OSSL_LIB_CTX *digest_context=NULL;OSSL_PROVIDER *digest_provider=NULL;EVP_MD *digest=fixed_sha256(&digest_context,&digest_provider);
|
|
363
|
+
struct stat interpreter_metadata,wrapper_metadata;
|
|
364
|
+
int interpreter=held_entry(field(entry,"interpreter",json_type_object),1,digest,&interpreter_metadata);
|
|
365
|
+
int wrapper=held_entry(field(entry,"wrapper",json_type_object),0,digest,&wrapper_metadata);
|
|
366
|
+
EVP_MD_free(digest);OSSL_PROVIDER_unload(digest_provider);OSSL_LIB_CTX_free(digest_context);
|
|
188
367
|
if(clearenv())deny("environment reset failed");
|
|
189
368
|
json_object *env=field(mapping,"worker_env",json_type_object);
|
|
190
369
|
json_object_object_foreach(env,key,value) {
|
|
@@ -195,5 +374,12 @@ int main(int argc,char **argv) {
|
|
|
195
374
|
setenv("ACE_ASSIGN_LAUNCH_MAPPING",argv[1],1))deny("authority context unavailable");
|
|
196
375
|
if(chdir(str(mapping,"worker_cwd")))deny("fixed worker directory unavailable");
|
|
197
376
|
close(fd);
|
|
198
|
-
|
|
377
|
+
struct stat final_interpreter,final_wrapper;
|
|
378
|
+
if(fstat(interpreter,&final_interpreter)||fstat(wrapper,&final_wrapper)||
|
|
379
|
+
!same_metadata(&interpreter_metadata,&final_interpreter)||!same_metadata(&wrapper_metadata,&final_wrapper))deny("worker entry changed before execution");
|
|
380
|
+
select_entry_descriptors(interpreter,wrapper);
|
|
381
|
+
parent_credentials(&original_parent,mapping);
|
|
382
|
+
char *const payload[]={"/proc/self/fd/4","-I","-S","-B","/proc/self/fd/5","authority","worker",NULL};
|
|
383
|
+
extern char **environ;
|
|
384
|
+
fexecve(4,payload,environ);deny("fixed worker execution failed");
|
|
199
385
|
}
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-runtime
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-16 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: fiddle
|
|
@@ -201,14 +201,24 @@ files:
|
|
|
201
201
|
- lib/ace/runtime/cli/commands/send.rb
|
|
202
202
|
- lib/ace/runtime/errors.rb
|
|
203
203
|
- lib/ace/runtime/molecules/cgroup_observation.rb
|
|
204
|
+
- lib/ace/runtime/molecules/codex_runtime_installation.rb
|
|
205
|
+
- lib/ace/runtime/molecules/execution_boot_baseline.rb
|
|
206
|
+
- lib/ace/runtime/molecules/execution_network_selection.rb
|
|
204
207
|
- lib/ace/runtime/molecules/execution_unit_installation.rb
|
|
208
|
+
- lib/ace/runtime/molecules/kernel_view_topology.rb
|
|
205
209
|
- lib/ace/runtime/molecules/linux_mount_info.rb
|
|
206
210
|
- lib/ace/runtime/molecules/network_installation_evidence.rb
|
|
211
|
+
- lib/ace/runtime/molecules/owned_child_scope.rb
|
|
207
212
|
- lib/ace/runtime/molecules/process_identity.rb
|
|
208
213
|
- lib/ace/runtime/molecules/protected_artifact_set.rb
|
|
209
214
|
- lib/ace/runtime/molecules/protected_linux.rb
|
|
210
215
|
- lib/ace/runtime/molecules/protected_socket.rb
|
|
216
|
+
- lib/ace/runtime/molecules/protected_task_context_entry.rb
|
|
217
|
+
- lib/ace/runtime/molecules/protected_worker_entry.rb
|
|
218
|
+
- lib/ace/runtime/molecules/readiness_configuration.rb
|
|
219
|
+
- lib/ace/runtime/molecules/readiness_runtime.rb
|
|
211
220
|
- lib/ace/runtime/molecules/runtime_selector.rb
|
|
221
|
+
- lib/ace/runtime/molecules/server_resource_observation.rb
|
|
212
222
|
- lib/ace/runtime/molecules/systemd_scope_manager.rb
|
|
213
223
|
- lib/ace/runtime/registry.rb
|
|
214
224
|
- lib/ace/runtime/testing.rb
|