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.
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 and json-c parser.
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
- and `libjson-c-dev` build packages. The executable requires the matching system
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; payload argv,
37
- cwd and environment come from the root-controlled mapping after authority release.
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 exact pinned server birth/parent, authenticates authority
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.
@@ -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
- cflags=$(env -i PATH=/usr/bin:/bin /usr/bin/pkg-config --cflags json-c)
12
- libs=$(env -i PATH=/usr/bin:/bin /usr/bin/pkg-config --libs json-c)
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) { return json_object_get_string(field(o,k,json_type_string)); }
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[4096]; size_t n=strlen(path); struct stat st;
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
- static void lineage(json_object *native) {
91
- json_object *server=field(native,"server_identity",json_type_object); int pid=number(server,"pid");
92
- if(getppid()!=pid) deny("gate is not the original server child");
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
- char birth[256]; snprintf(birth,sizeof(birth),"linux:%s:%s",boot,ticks);
100
- if(strcmp(birth,str(server,"started_at")))deny("server incarnation changed");
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
- credentials(pid,number(server,"uid"),number(server,"gid"),field(server,"groups",json_type_array));
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/v1"))deny("unsupported deployment map");
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
- lineage(field(mapping,"native",json_type_object));
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
- policy();lineage(field(mapping,"native",json_type_object));credentials(peer.pid,authority,number(service,"gid"),field(service,"groups",json_type_array));
183
- json_object *arguments=field(mapping,"worker_argv",json_type_array);size_t count=json_object_array_length(arguments);
184
- if(!count||count>64)deny("invalid fixed payload");
185
- char **payload=calloc(count+1,sizeof(char *));if(!payload)deny("allocation failed");
186
- for(size_t i=0;i<count;i++) {json_object *value=json_object_array_get_idx(arguments,i);if(!json_object_is_type(value,json_type_string))deny("invalid fixed argv");payload[i]=(char *)json_object_get_string(value);}
187
- root_path(payload[0],0);struct stat program;if(stat(payload[0],&program)||(program.st_mode&06000)||!(program.st_mode&0111))deny("unsafe payload executable");
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
- execv(payload[0],payload);deny("fixed worker execution failed");
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.2.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-05 00:00:00.000000000 Z
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