ace-runtime 0.1.1 → 0.3.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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -0
  3. data/README.md +9 -0
  4. data/docs/usage.md +123 -0
  5. data/lib/ace/runtime/molecules/cgroup_observation.rb +148 -0
  6. data/lib/ace/runtime/molecules/codex_runtime_installation.rb +130 -0
  7. data/lib/ace/runtime/molecules/execution_boot_baseline.rb +134 -0
  8. data/lib/ace/runtime/molecules/execution_network_selection.rb +85 -0
  9. data/lib/ace/runtime/molecules/execution_unit_installation.rb +663 -0
  10. data/lib/ace/runtime/molecules/kernel_view_topology.rb +133 -0
  11. data/lib/ace/runtime/molecules/linux_mount_info.rb +101 -0
  12. data/lib/ace/runtime/molecules/network_installation_evidence.rb +332 -0
  13. data/lib/ace/runtime/molecules/process_identity.rb +123 -0
  14. data/lib/ace/runtime/molecules/protected_artifact_set.rb +211 -0
  15. data/lib/ace/runtime/molecules/protected_linux.rb +118 -0
  16. data/lib/ace/runtime/molecules/protected_socket.rb +119 -0
  17. data/lib/ace/runtime/molecules/protected_task_context_entry.rb +162 -0
  18. data/lib/ace/runtime/molecules/protected_worker_entry.rb +32 -0
  19. data/lib/ace/runtime/molecules/readiness_configuration.rb +154 -0
  20. data/lib/ace/runtime/molecules/readiness_runtime.rb +94 -0
  21. data/lib/ace/runtime/molecules/server_resource_observation.rb +183 -0
  22. data/lib/ace/runtime/molecules/systemd_scope_manager.rb +411 -0
  23. data/lib/ace/runtime/testing/adapter_contract.rb +9 -0
  24. data/lib/ace/runtime/version.rb +1 -1
  25. data/lib/ace/runtime.rb +3 -0
  26. data/native/README.md +124 -0
  27. data/native/build-worker-gate +20 -0
  28. data/native/worker_gate.c +385 -0
  29. metadata +63 -2
@@ -0,0 +1,385 @@
1
+ #define _GNU_SOURCE
2
+ #include <errno.h>
3
+ #include <fcntl.h>
4
+ #include <json-c/json.h>
5
+ #include <poll.h>
6
+ #include <openssl/crypto.h>
7
+ #include <openssl/evp.h>
8
+ #include <openssl/provider.h>
9
+ #include <sys/mman.h>
10
+ #include <stdio.h>
11
+ #include <stdint.h>
12
+ #include <stdlib.h>
13
+ #include <string.h>
14
+ #include <sys/prctl.h>
15
+ #include <sys/socket.h>
16
+ #include <sys/stat.h>
17
+ #include <sys/un.h>
18
+ #include <time.h>
19
+ #include <unistd.h>
20
+ #include <grp.h>
21
+
22
+ #define MAP_PATH "/etc/ace/assignment-authorities.json"
23
+ #define LIMIT 65536
24
+ static _Noreturn void deny(const char *why) { fprintf(stderr, "ace-worker-gate: %s\n", why); exit(125); }
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; }
26
+ static int token(const char *s) {
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;
29
+ for (const unsigned char *p=(const unsigned char *)s; *p; p++)
30
+ if (!((*p>='a'&&*p<='z')||(*p>='A'&&*p<='Z')||(*p>='0'&&*p<='9')||*p=='_'||*p=='-'||*p=='.')) return 0;
31
+ return 1;
32
+ }
33
+ static json_object *field(json_object *o,const char *k,enum json_type t) {
34
+ json_object *v=NULL;
35
+ if (!o || !json_object_object_get_ex(o,k,&v) || !json_object_is_type(v,t)) deny("invalid protected schema");
36
+ return v;
37
+ }
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
+ }
43
+ static int number(json_object *o,const char *k) {
44
+ int64_t n=json_object_get_int64(field(o,k,json_type_int));
45
+ if (n<=0 || n>2147483647) deny("invalid principal identity");
46
+ return (int)n;
47
+ }
48
+ static void trusted_path(const char *path,int dir,uid_t owner) {
49
+ char copy[4097]; size_t n=strlen(path); struct stat st;
50
+ if (!n || n>=sizeof(copy) || path[0]!='/' || strstr(path,"//") || strstr(path,"/../") || strstr(path,"/./") ||
51
+ !strcmp(path+n-1,"/") || !strcmp(path+n-(n>=3?3:n),"/..")) deny("invalid protected path");
52
+ memcpy(copy,path,n+1);
53
+ for (size_t i=1;i<=n;i++) if (copy[i]=='/' || i==n) {
54
+ char c=copy[i]; copy[i]=0;
55
+ if (lstat(copy,&st) || (i==n ? st.st_uid!=owner : (st.st_uid!=0 && st.st_uid!=owner)) || (st.st_mode&022) || S_ISLNK(st.st_mode) ||
56
+ (i<n&&!S_ISDIR(st.st_mode))) deny("protected path is writable or replaced");
57
+ copy[i]=c;
58
+ }
59
+ if (dir && !S_ISDIR(st.st_mode)) deny("protected directory unavailable");
60
+ if (!dir && !S_ISREG(st.st_mode)) deny("protected file unavailable");
61
+ }
62
+ static void root_path(const char *path,int dir) { trusted_path(path,dir,0); }
63
+ static char *read_file(const char *path,size_t max) {
64
+ int fd=open(path,O_RDONLY|O_CLOEXEC|O_NOFOLLOW); if(fd<0) deny("policy file unavailable");
65
+ char *s=calloc(max+1,1); if(!s) deny("allocation failed"); size_t n=0;
66
+ while(n<max) { ssize_t r=read(fd,s+n,max-n); if(r<0&&errno==EINTR) continue; if(r<0) deny("policy read failed"); if(!r) break; n+=(size_t)r; }
67
+ char extra; if(n==max&&read(fd,&extra,1)!=0) deny("policy file oversized"); close(fd); return s;
68
+ }
69
+ static void policy(void) {
70
+ char *s=read_file("/proc/sys/kernel/yama/ptrace_scope",16);
71
+ if(strcmp(s,"2\n")&&strcmp(s,"2")) deny("Linux Yama ptrace_scope=2 is required");
72
+ free(s);
73
+ }
74
+ static void credentials(pid_t pid,int uid,int gid,json_object *groups) {
75
+ char path[64]; snprintf(path,sizeof(path),"/proc/%d/status",pid); char *s=read_file(path,16384);
76
+ int got_uid=0,got_gid=0,caps=0,got_groups=0,no_new_privs=0; char *save=NULL;
77
+ for(char *line=strtok_r(s,"\n",&save);line;line=strtok_r(NULL,"\n",&save)) {
78
+ unsigned a,b,c,d; unsigned long long cap;
79
+ if(sscanf(line,"Uid: %u %u %u %u",&a,&b,&c,&d)==4) { if(a!=(unsigned)uid||b!=a||c!=a||d!=a) deny("UID changed"); got_uid=1; }
80
+ if(sscanf(line,"Gid: %u %u %u %u",&a,&b,&c,&d)==4) { if(a!=(unsigned)gid||b!=a||c!=a||d!=a) deny("GID changed"); got_gid=1; }
81
+ if(sscanf(line,"NoNewPrivs: %u",&a)==1) { if(a!=1)deny("NoNewPrivs=1 is required"); no_new_privs=1; }
82
+ const char *names[]={"CapInh:","CapPrm:","CapEff:","CapBnd:","CapAmb:"};
83
+ for(int i=0;i<5;i++) if(!strncmp(line,names[i],7)) {
84
+ if(sscanf(line+7,"%llx",&cap)!=1 || cap!=0) deny("all capability sets must be empty");
85
+ caps|=1<<i;
86
+ }
87
+ if(!strncmp(line,"Groups:",7)) {
88
+ size_t count=json_object_array_length(groups),index=0; char *end=NULL,*p=line+7;
89
+ while(1) { while(*p==' '||*p=='\t')p++; if(!*p)break; errno=0; long group=strtol(p,&end,10);
90
+ if(errno||end==p||index>=count||!json_object_is_type(json_object_array_get_idx(groups,index),json_type_int)||
91
+ group!=json_object_get_int64(json_object_array_get_idx(groups,index))) deny("supplementary groups changed");
92
+ p=end; index++;
93
+ }
94
+ if(index!=count) deny("supplementary groups changed");
95
+ got_groups=1;
96
+ }
97
+ }
98
+ free(s); if(!got_uid||!got_gid||!got_groups||!no_new_privs||caps!=31)deny("kernel credentials unavailable");
99
+ }
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");
103
+ char path[64]; snprintf(path,sizeof(path),"/proc/%d/stat",pid); char *s=read_file(path,16384);
104
+ char *end=strrchr(s,')'); if(!end||end[1]!=' ') deny("server birth unavailable");
105
+ char *save=NULL,*word=strtok_r(end+2," ", &save); int i=3; const char *ticks=NULL;
106
+ while(word) { if(i==22){ticks=word;break;} i++;word=strtok_r(NULL," ",&save); }
107
+ if(!ticks||strspn(ticks,"0123456789")!=strlen(ticks))deny("server birth unavailable");
108
+ char *boot=read_file("/proc/sys/kernel/random/boot_id",128); boot[strcspn(boot,"\n")]=0;
109
+ int written=snprintf(birth,256,"linux:%s:%s",boot,ticks);
110
+ if(written<0||written>=256)deny("parent birth oversized");
111
+ free(boot);free(s);
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);
273
+ }
274
+ static void wait_fd(int fd,short events,double deadline) {
275
+ struct pollfd p={.fd=fd,.events=events}; int r;
276
+ do {
277
+ double remaining=deadline-now(); if(remaining<=0)deny("authority deadline expired");
278
+ r=poll(&p,1,(int)(remaining*1000));
279
+ }while(r<0&&errno==EINTR);
280
+ if(r<=0 || (p.revents&(POLLERR|POLLNVAL)))deny("authority stream unavailable");
281
+ }
282
+ static void send_frame(int fd,json_object *o,double deadline) {
283
+ const char *s=json_object_to_json_string_ext(o,JSON_C_TO_STRING_PLAIN); size_t n=strlen(s);
284
+ if(n+1>LIMIT)deny("request oversized");
285
+ char *frame=malloc(n+2); if(!frame)deny("allocation failed");
286
+ memcpy(frame,s,n);frame[n++]='\n';size_t offset=0;
287
+ while(offset<n) { wait_fd(fd,POLLOUT,deadline); ssize_t r=send(fd,frame+offset,n-offset,MSG_NOSIGNAL);
288
+ if(r<0&&(errno==EINTR||errno==EAGAIN))continue;
289
+ if(r<=0)deny("authority write failed");
290
+ offset+=(size_t)r; }
291
+ free(frame);
292
+ }
293
+ static json_object *receive(int fd,double deadline) {
294
+ char *s=calloc(LIMIT+1,1); if(!s)deny("allocation failed"); size_t n=0;
295
+ while(n<LIMIT) { wait_fd(fd,POLLIN,deadline); ssize_t r=recv(fd,s+n,1,0);
296
+ if(r<0&&(errno==EINTR||errno==EAGAIN))continue;
297
+ if(r<=0)deny("authority EOF before permission");
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);
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");
304
+ json_tokener_free(t);free(s);return o; }
305
+ }
306
+ deny("authority frame oversized"); return NULL;
307
+ }
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);
311
+ if(prctl(PR_SET_DUMPABLE,0,0,0,0))deny("non-dumpable gate unavailable");
312
+ if(argc!=3||!token(argv[1])||!token(argv[2]))deny("expected fixed mapping and correlation ticket");
313
+ policy();root_path(MAP_PATH,0);char *bytes=read_file(MAP_PATH,LIMIT);
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));
315
+ if(!map||json_tokener_get_error(parser)!=json_tokener_success||json_tokener_get_parse_end(parser)!=strlen(bytes))deny("invalid deployment map");
316
+ json_tokener_free(parser);free(bytes);
317
+ if(strcmp(str(map,"schema"),"ace.assign.authorities/v2"))deny("unsupported deployment map");
318
+ json_object *mapping=field(field(map,"launch_mappings",json_type_object),argv[1],json_type_object);
319
+ json_object *service=field(field(map,"authorities",json_type_object),str(mapping,"authority_id"),json_type_object);
320
+ int worker=number(mapping,"worker_uid"),launcher=number(mapping,"launcher_uid"),authority=number(service,"uid");
321
+ if(worker==launcher||worker==authority||authority==launcher)deny("principals are not distinct");
322
+ credentials(getpid(),worker,number(mapping,"worker_gid"),field(mapping,"worker_groups",json_type_array));
323
+ parent_credentials(&original_parent,mapping);
324
+ const char *bootstrap=str(mapping,"bootstrap");root_path(bootstrap,0);
325
+ struct stat installed,self;
326
+ if(stat(bootstrap,&installed)||stat("/proc/self/exe",&self)||installed.st_dev!=self.st_dev||installed.st_ino!=self.st_ino||
327
+ (installed.st_mode&06000))deny("gate executable differs from installed bootstrap");
328
+ const char *path=str(service,"socket_path");char parent[4096];if(strlen(path)>=sizeof(parent))deny("socket path too long");
329
+ strcpy(parent,path);char *slash=strrchr(parent,'/');if(!slash||slash==parent)deny("invalid socket path");*slash=0;
330
+ /* Only root/the trusted authority may own the non-writable ancestry. */
331
+ trusted_path(parent,1,(uid_t)authority);
332
+ struct stat directory,endpoint;if(lstat(parent,&directory)||!S_ISDIR(directory.st_mode)||directory.st_uid!=(uid_t)authority||
333
+ (directory.st_mode&022)||lstat(path,&endpoint)||!S_ISSOCK(endpoint.st_mode)||endpoint.st_uid!=(uid_t)authority)deny("unsafe authority endpoint");
334
+ int fd=socket(AF_UNIX,SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK,0);if(fd<0)deny("socket unavailable");
335
+ struct sockaddr_un address={.sun_family=AF_UNIX};if(strlen(path)>=sizeof(address.sun_path))deny("socket path too long");strcpy(address.sun_path,path);
336
+ double deadline=now()+30;
337
+ if(connect(fd,(struct sockaddr *)&address,sizeof(address))<0) {
338
+ if(errno!=EINPROGRESS&&errno!=EAGAIN)deny("authority connection failed");
339
+ wait_fd(fd,POLLOUT,deadline);
340
+ int error=0;socklen_t length=sizeof(error);if(getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&length)||error)deny("authority connection failed");
341
+ }
342
+ struct ucred peer;socklen_t length=sizeof(peer);
343
+ if(getsockopt(fd,SOL_SOCKET,SO_PEERCRED,&peer,&length)||peer.uid!=(uid_t)authority||peer.gid!=(gid_t)number(service,"gid"))deny("authority peer differs");
344
+ credentials(peer.pid,authority,number(service,"gid"),field(service,"groups",json_type_array));
345
+ struct stat after;if(lstat(path,&after)||after.st_dev!=endpoint.st_dev||after.st_ino!=endpoint.st_ino||after.st_uid!=endpoint.st_uid)deny("authority endpoint replaced");
346
+ json_object *request=json_object_new_object(),*params=json_object_new_object();
347
+ json_object_object_add(request,"version",json_object_new_int(1));json_object_object_add(request,"operation",json_object_new_string("gate_ready"));
348
+ json_object_object_add(request,"mutation_id",NULL);json_object_object_add(request,"project_id",json_object_new_string(str(mapping,"project_id")));
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]));
350
+ json_object_object_add(request,"params",params);parent_credentials(&original_parent,mapping);send_frame(fd,request,deadline);json_object_put(request);
351
+ json_object *ready=receive(fd,deadline);
352
+ if(strcmp(str(ready,"status"),"ok")||strcmp(str(field(ready,"data",json_type_object),"phase"),"ready"))deny("gate not admitted");
353
+ json_object_put(ready);
354
+ json_object *permission=receive(fd,deadline);
355
+ if(strcmp(str(permission,"operation"),"release")||strcmp(str(permission,"launch_ticket"),argv[2])||
356
+ !token(str(permission,"attempt_id"))||!token(str(permission,"assignment_id"))||!token(str(permission,"journal_commit")))deny("release permission differs");
357
+ if(json_object_get_int64(field(permission,"generation",json_type_int))<1)deny("invalid release generation");
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);
367
+ if(clearenv())deny("environment reset failed");
368
+ json_object *env=field(mapping,"worker_env",json_type_object);
369
+ json_object_object_foreach(env,key,value) {
370
+ if(!json_object_is_type(value,json_type_string)||!strncmp(key,"LD_",3)||!strncmp(key,"DYLD_",5)||!strncmp(key,"RUBY",4)||!strncmp(key,"BUNDLE",6)||!strncmp(key,"PYTHON",6))deny("unsafe fixed environment");
371
+ if(setenv(key,json_object_get_string(value),1))deny("environment setup failed");
372
+ }
373
+ if(setenv("ACE_ASSIGN_ATTEMPT_ID",str(permission,"attempt_id"),1)||setenv("ACE_ASSIGN_ASSIGNMENT_ID",str(permission,"assignment_id"),1)||
374
+ setenv("ACE_ASSIGN_LAUNCH_MAPPING",argv[1],1))deny("authority context unavailable");
375
+ if(chdir(str(mapping,"worker_cwd")))deny("fixed worker directory unavailable");
376
+ close(fd);
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");
385
+ }
metadata CHANGED
@@ -1,14 +1,54 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-runtime
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-10-04 00:00:00.000000000 Z
10
+ date: 2026-10-10 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: fiddle
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '1.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2'
32
+ - !ruby/object:Gem::Dependency
33
+ name: json
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '2.20'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '3'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '2.20'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '3'
12
52
  - !ruby/object:Gem::Dependency
13
53
  name: ace-support-cli
14
54
  requirement: !ruby/object:Gem::Requirement
@@ -160,12 +200,33 @@ files:
160
200
  - lib/ace/runtime/cli.rb
161
201
  - lib/ace/runtime/cli/commands/send.rb
162
202
  - lib/ace/runtime/errors.rb
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
207
+ - lib/ace/runtime/molecules/execution_unit_installation.rb
208
+ - lib/ace/runtime/molecules/kernel_view_topology.rb
209
+ - lib/ace/runtime/molecules/linux_mount_info.rb
210
+ - lib/ace/runtime/molecules/network_installation_evidence.rb
211
+ - lib/ace/runtime/molecules/process_identity.rb
212
+ - lib/ace/runtime/molecules/protected_artifact_set.rb
213
+ - lib/ace/runtime/molecules/protected_linux.rb
214
+ - lib/ace/runtime/molecules/protected_socket.rb
215
+ - lib/ace/runtime/molecules/protected_task_context_entry.rb
216
+ - lib/ace/runtime/molecules/protected_worker_entry.rb
217
+ - lib/ace/runtime/molecules/readiness_configuration.rb
218
+ - lib/ace/runtime/molecules/readiness_runtime.rb
163
219
  - lib/ace/runtime/molecules/runtime_selector.rb
220
+ - lib/ace/runtime/molecules/server_resource_observation.rb
221
+ - lib/ace/runtime/molecules/systemd_scope_manager.rb
164
222
  - lib/ace/runtime/registry.rb
165
223
  - lib/ace/runtime/testing.rb
166
224
  - lib/ace/runtime/testing/adapter_contract.rb
167
225
  - lib/ace/runtime/testing/scripted_runtime.rb
168
226
  - lib/ace/runtime/version.rb
227
+ - native/README.md
228
+ - native/build-worker-gate
229
+ - native/worker_gate.c
169
230
  homepage: https://github.com/cs3b/ace
170
231
  licenses:
171
232
  - MIT