Skip to content

Commit eaecf22

Browse files
committed
ci: prepare approved public Unicode C and Rust pilot
Add the reviewed public-source workflow and pinned helpers for the original four-case C Unicode suite against C and Rust normalizers at 62a3294. Keep the request unarmed; a separate manifest-only activation is required. Use sequential standard Ubuntu public runners, contents:read permissions and text logs only, with a maximum 260 runner-minute setup/activation budget. No paid runners, artifact/cache uploads, secrets or kernel test/timeout changes. A passing logs-only pair remains provisional without retained raw ELF artifacts.
1 parent 2099a6c commit eaecf22

32 files changed

Lines changed: 3279 additions & 0 deletions
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"schema": 1,
3+
"request_id": "unicode-public-62a3294-v1",
4+
"source_sha": "62a3294181556f17db863c9016524fa3068a374b",
5+
"profile": "unicode-norm-original-c-x86_64-v1",
6+
"evidence": "logs-only",
7+
"armed": false
8+
}

‎.github/pilots/unicode-v1/CompareEvidence.pm‎

Lines changed: 201 additions & 0 deletions
Large diffs are not rendered by default.

‎.github/pilots/unicode-v1/Elf64.pm‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package Elf64;
2+
# SPDX-License-Identifier: GPL-2.0-only
3+
# Newly authored minimal reader of the public ELF64 little-endian format.
4+
# Supports this x86_64 non-LTO pilot only. Unsupported inputs fail closed.
5+
use strict; use warnings;
6+
sub new {
7+
my ($class, $path) = @_;
8+
open my $fh, '<:raw', $path or die "$path: $!"; local $/; my $data = <$fh>;
9+
die "not ELF64 LE x86_64: $path\n" unless substr($data,0,6) eq "\x7fELF\x02\x01" && unpack('v',substr($data,18,2)) == 62;
10+
my ($base,$size,$count,$names) = (unpack('Q<',substr($data,40,8)),unpack('v',substr($data,58,2)),unpack('v',substr($data,60,2)),unpack('v',substr($data,62,2)));
11+
die "unsupported section numbering\n" unless $size == 64 && $count && $names < $count;
12+
my $self = bless {data=>$data,sections=>[],symbols=>[],rels=>[]}, $class;
13+
for my $i (0..$count-1) {
14+
my @v = unpack('V V Q< Q< Q< Q< V V Q< Q<',substr($data,$base+$i*64,64));
15+
push @{$self->{sections}}, {index=>$i,nameoff=>$v[0],type=>$v[1],flags=>$v[2],addr=>$v[3],offset=>$v[4],size=>$v[5],link=>$v[6],info=>$v[7],entry=>$v[9]};
16+
}
17+
my $strings=$self->section_bytes($names);
18+
for (@{$self->{sections}}) { $_->{name}=cstr($strings,$_->{nameoff}); }
19+
for my $sec (@{$self->{sections}}) {
20+
next unless $sec->{type} == 2;
21+
die "unsupported multiple symbol tables\n" if @{$self->{symbols}};
22+
die "bad symbol size\n" unless $sec->{entry} == 24;
23+
my $names=$self->section_bytes($sec->{link}); my $raw=$self->section_bytes($sec->{index});
24+
for (my $i=0;$i<length($raw);$i+=24) {
25+
my ($n,$info,$other,$section,$value,$len)=unpack('V C C v Q< Q<',substr($raw,$i,24));
26+
push @{$self->{symbols}}, {name=>cstr($names,$n),type=>$info & 15,bind=>$info>>4,section=>$section,value=>$value,size=>$len};
27+
}
28+
$self->{symtab}=$sec->{index};
29+
}
30+
die "missing full symbols\n" unless @{$self->{symbols}};
31+
for my $sec (@{$self->{sections}}) {
32+
next unless $sec->{type} == 4;
33+
die "unsupported relocation table\n" unless $sec->{entry} == 24 && $sec->{link} == $self->{symtab};
34+
my $raw=$self->section_bytes($sec->{index});
35+
for (my $i=0;$i<length($raw);$i+=24) {
36+
my ($offset,$info,$addend)=unpack('Q< Q< q<',substr($raw,$i,24));
37+
push @{$self->{rels}}, {section=>$sec->{info},offset=>$offset,type=>$info & 0xffffffff,symbol=>$self->{symbols}[$info>>32],addend=>$addend};
38+
}
39+
}
40+
return $self;
41+
}
42+
sub cstr { my ($data,$at)=@_; die "string out of range\n" if $at>=length($data); my $end=index($data,"\0",$at); die "unterminated string\n" if $end<0; return substr($data,$at,$end-$at); }
43+
sub section_bytes { my ($s,$i)=@_; my $sec=$s->{sections}[$i] // die "section out of range\n"; die "NOBITS not supported\n" if $sec->{type}==8; return substr($s->{data},$sec->{offset},$sec->{size}); }
44+
sub symbols { @{$_[0]->{symbols}} }
45+
sub one {
46+
my ($s,$name)=@_; my @m=grep { $_->{name} eq $name && $_->{section}>0 && $_->{section}<0xff00 } $s->symbols;
47+
die "expected one definition of $name, got ".scalar(@m)."\n" unless @m==1; return $m[0];
48+
}
49+
sub symbol_bytes { my ($s,$sym)=@_; my $sec=$s->{sections}[$sym->{section}]; return substr($s->section_bytes($sym->{section}),$sym->{value}-$sec->{addr},$sym->{size}); }
50+
sub at {
51+
my ($s,$addr,$len)=@_;
52+
for my $sec (@{$s->{sections}}) {
53+
next unless $sec->{type} != 8 && $sec->{flags}&2 && $addr >= $sec->{addr} && $addr+$len <= $sec->{addr}+$sec->{size};
54+
return substr($s->{data},$sec->{offset}+$addr-$sec->{addr},$len);
55+
}
56+
die "address not in file-backed allocated section\n";
57+
}
58+
sub string_at {
59+
my ($s,$addr)=@_; my $result='';
60+
for (0..4095) { my $b=$s->at($addr+$_,1); return $result if $b eq "\0"; $result.=$b; }
61+
die "final string too long\n";
62+
}
63+
sub rels_in { my ($s,$sym)=@_; return grep { $_->{section}==$sym->{section} && $_->{offset}>=$sym->{value} && $_->{offset}<$sym->{value}+$sym->{size} } @{$s->{rels}}; }
64+
1;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package Evidence;
2+
# SPDX-License-Identifier: GPL-2.0-only
3+
use strict; use warnings; use JSON::PP; use Digest::SHA qw(sha256_hex); use Encode qw(encode decode FB_CROAK);
4+
our $JSON=JSON::PP->new->canonical->ascii;
5+
our @STAGES=qw(install configure build noop ownership guest parse);
6+
our @CASES=qw(check_supported_versions check_utf8_comparisons check_utf8_nfdicf check_utf8_nfdi);
7+
our @APIS=qw(utf8version_is_supported utf8nlen utf8ncursor utf8byte);
8+
our $SOURCE='62a3294181556f17db863c9016524fa3068a374b';
9+
sub roles {
10+
my @r;
11+
for my $s (qw(pull container)) {for my $kind (qw(command log status resources.tsv)) {push @r,["host.$s.$kind","host/$s.$kind"]}}
12+
push @r,map { ["host.$_","host/$_"] } qw(preflight.txt terminal.txt image.txt container.cid cleanup.inspect cleanup.stderr cleanup.status result.status);
13+
push @r,map { ["runtime.$_","runtime/$_"] } qw(context.json source.lock source.before.txt source.after.txt apt-suites.tsv base-packages.tsv downloaded-packages.tsv installed-packages.tsv tool-versions.txt tool-binaries.sha256 config.full config-check.txt);
14+
for my $s (@STAGES) {for my $kind (qw(command log status resources.tsv)) {push @r,["runtime.$s.$kind","runtime/$s.$kind"]}}
15+
push @r,map { ["runtime.$_","runtime/$_"] } qw(O-before-noop.sha256 O-after-noop.sha256 O-before-guest.sha256 O-after-guest.sha256 O-links-before-noop.tsv O-links-after-noop.tsv O-links-before-guest.tsv O-links-after-guest.tsv image-identities.sha256 object-identities.sha256 test.cmd core.cmd data.cmd crc.cmd norm.cmd guest-admission.json strict-observer.txt strict-observer.stderr upstream-result.json upstream-parser.txt observer-parser.status member.status);
16+
return @r;
17+
}
18+
sub may_be_empty { return $_[0] =~ /\A(?:host\.cleanup\.(?:inspect|stderr)|runtime\.strict-observer\.stderr)\z/; }
19+
sub slurp {my($p)=@_;open my $f,'<:raw',$p or die "$p: $!";local $/;return <$f>}
20+
sub exact_keys {my($h,@keys)=@_;die "not an object\n" unless ref($h) eq 'HASH';die "unexpected object fields\n" unless join(',',sort keys %$h) eq join(',',sort @keys)}
21+
sub config {
22+
my($s)=@_;my %c;
23+
for(split /\n/,$s){my($k,$v);if(/^(CONFIG_\w+)=(.*)$/){($k,$v)=($1,$2)}elsif(/^# (CONFIG_\w+) is not set$/){($k,$v)=($1,'n')}else{next}die "duplicate config $k\n" if exists$c{$k};$c{$k}=$v}
24+
die "empty config\n" unless keys%c;return \%c;
25+
}
26+
sub hashes {
27+
my($s)=@_;my %h;
28+
for(split /\n/,$s){die "invalid hash entry\n" unless /^([0-9a-f]{64}) ([^\r\n]+)$/;my($v,$p)=($1,$2);die "duplicate hash path\n" if exists$h{$p};$h{$p}=$v}
29+
die "empty hash list\n" unless keys%h;return \%h;
30+
}
31+
sub resources {
32+
my($text)=@_;my @rows=split /\n/,$text;
33+
die "resource header\n" unless shift(@rows) eq "phase\tutc\tfree_bytes\tavailable_memory\tfree_inodes\tO_allocated_bytes\tevidence_allocated_bytes";
34+
die "resource endpoints\n" unless @rows>=2 && $rows[0]=~/^admission\t/ && $rows[-1]=~/^terminal\t/;
35+
for my $i(0..$#rows){my @v=split /\t/,$rows[$i];die "resource row\n" unless @v==7 && $v[0] eq ($i==0?'admission':$i==$#rows?'terminal':'live') && $v[1]=~/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ$/;
36+
for(@v[2..6]){die "resource number\n" unless /^\d+$/}
37+
die "resource cap/floor failure\n" unless $v[2]>=1073741824 && $v[3]>=536870912 && $v[4]>=10000 && $v[5]<=1342177280 && $v[6]<=134209536;
38+
die "resource admission failure\n" if $i==0 && ($v[2]<4294967296 || $v[3]<6442450944);
39+
}
40+
}
41+
1;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Unarmed public Unicode pilot: revision v4 of the unreleased evidence protocol
2+
3+
This is a review candidate. No publication or run is authorized by these files.
4+
The source remains `62a3294181556f17db863c9016524fa3068a374b`; original C tests,
5+
config values, provider selectors, flags and guest settings retain their reviewed
6+
profile. The setup-base gate is deliberately stale and must be refreshed to the
7+
then-verified feature-branch tip after review. Do not change the experiment pin.
8+
9+
Both matrix jobs reject `github.run_attempt != 1` before checkout, and reject
10+
reruns again in the host, container, installer and stage shell boundaries. A
11+
rerun requires a new separately reviewed request. The initial launch record is
12+
still unapproved and the request manifest is still unarmed.
13+
14+
The installer uses strict `apt-get --error-on=any update` and requires exactly
15+
one matching InRelease for each of the three reviewed suites. All three exact
16+
hashes are checked after apt's signature and default validity/time checks. No
17+
insecure/trusted/freshness/TLS bypass is added. The earliest expiry is updates
18+
metadata on 2026-10-09 at 14:11:18 UTC. Expiry must fail and return for review.
19+
20+
Docker CID bytes are preserved as Docker writes them: exactly 64 lowercase hex
21+
characters with no newline. The incorrect older newline fixture is retained as
22+
a negative control. No whitespace normalization changes the recorded file.
23+
24+
Image pull and the full container lifetime are supervised. Installation,
25+
configuration, build, no-op, ownership audit, guest and parser have individual
26+
admission/live/terminal samples and explicit native/supervisor/cleanup/interruption
27+
statuses. Cleanup targets the owned process group even when its leader exits
28+
first. Docker cleanup is bounded, records errors and fails on uncertainty. The
29+
host separately checks complete output/evidence caps at termination. The output
30+
cap stays 1.25 GiB, evidence cap 128 MiB, minimum free disk 1 GiB and minimum
31+
available memory 512 MiB. Admission retains 4 GiB disk and 6 GiB available memory;
32+
the initial host admission requires 10 GiB disk. A small evidence reserve covers
33+
final status writes. Cap measurements use allocated filesystem bytes. Every
34+
measurement, parse, clock read and record write checks its own return status;
35+
partial numeric output from a failed command is rejected even when Bash errexit
36+
is disabled. No evidence is truncated to produce a pass.
37+
38+
## Proposed run and time commitment
39+
40+
The setup commit adds both this workflow and the unarmed manifest. It can trigger
41+
one request-only run, which is expected to reject the setup push and start no
42+
member. After that expected outcome is verified, a separate manifest-only
43+
activation commit can trigger one request job and the sequential C/R pair.
44+
There is no skip directive or hidden trigger bypass. Approval must cover both
45+
commits, both possible workflow runs and the specified public logs.
46+
47+
The request job has a 10-minute ceiling in each run. Each member has a proposed
48+
120-minute GitHub job ceiling. Thus the complete planned setup-plus-activation
49+
maximum is 260 standard-runner minutes: 10 + 10 + 120 + 120. It is not a measured
50+
duration. The verified prospective base had no pre-existing workflow files; the
51+
head and workflow inventory must be rechecked before publication.
52+
53+
The internal soft job deadline is 115 minutes from the first workflow step,
54+
leaving a five-minute tail relative to that step's 120-minute window. Platform
55+
setup or cancellation is outside this internal timer; a hard runner cancellation
56+
remains a failed attempt. Pull is allowed at most 10 minutes. The container
57+
aggregate is at most 100 minutes and must end at least three minutes before the
58+
soft job deadline. Native stage allowances are installation 20, configuration 10,
59+
build 30, no-op 10, ownership 5, guest supervision 17 and parser 5 minutes.
60+
61+
The build allowance is over four times the recorded 6m54s native Rust build,
62+
while package/network/CI performance remains unmeasured. Stage ceilings share
63+
the aggregate budget; they are not promises that every stage can consume its
64+
maximum. A stage overrun fails the pilot instead of changing its work.
65+
66+
The original KUnit timeout remains 300 seconds. The original QEMU outer timeout
67+
remains exactly 900 seconds with a 10-second kill grace. Its separate supervisor
68+
allowance is 1,020 seconds, so a 900-second stage wrapper cannot preempt the grace
69+
or postchecks. Before starting the guest, a recorded admission requires at least
70+
1,560 seconds remaining in both the container and soft job budgets: 1,020 guest
71+
supervision + 300 parser + 60 cleanup + 180 evidence seconds. Insufficient budget
72+
stops before guest launch. No guest clock, workload or original timeout is tuned.
73+
74+
The unchanged original Python KUnit parser and the new strict observer both run.
75+
Their outputs are written directly, with both statuses and nonempty required
76+
results checked. There is no tee pipeline whose failure can be lost.
77+
78+
## Complete text evidence
79+
80+
`Evidence.pm` is the exact ordered role catalog. `emit-evidence.pl` emits only
81+
those text files using `UEV3` framing. It records file identity, UTF-8 byte count,
82+
SHA256, ordered text chunks and an ordered manifest digest. No binary image,
83+
object, archive or encoded binary payload is emitted.
84+
85+
The envelope includes full raw stage logs, exact command arrays, resource samples,
86+
status/cleanup records, source/config/tool/apt evidence, complete before/after
87+
output-tree hash and symlink inventories, original C command files and object
88+
identities, the complete selected normalizer command sidecar, final ELF-owner
89+
observations, raw serial, strict observer output and
90+
unchanged upstream parser JSON. The selected normalizer command's source/required
91+
flags are checked and its exact bytes are bound to the output-tree hash inventory;
92+
C and Rust normalizer command files are not required to be identical.
93+
Empty diagnostic files are allowed only for
94+
explicitly designated stderr/cleanup roles. Any missing file, cap overrun or
95+
encoding/emission failure prevents success. Failures remain visible in the logs.
96+
97+
## Read-only download and comparison
98+
99+
After an authorized run, first verify its repository, event, feature branch,
100+
approved activation commit, attempt 1, workflow identity and the two unique jobs
101+
`unicode-c` and `unicode-rust` through authenticated read-only GitHub metadata.
102+
Both jobs must be terminal successes. Preserve the fetched job text exactly as
103+
UTF-8 and hash those saved files before comparison. Populate the example receipt
104+
from that verified metadata and those local file hashes; never populate it from
105+
success text in the logs themselves.
106+
107+
When a connector returns decoded text, the preserved bytes are the UTF-8 encoding
108+
of that decoded text. They are not claimed to be original HTTP response bytes.
109+
If the connector omits or truncates text, obtain a complete supported read or
110+
stop; do not reconstruct or assume missing evidence. Retain the download receipt
111+
and original saved text with the existing approved backup process.
112+
113+
Run:
114+
115+
perl compare-logs.pl trusted-download-receipt.json C-job.log Rust-job.log
116+
117+
The comparator checks the download hashes before recognizing optional GitHub
118+
timestamp prefixes. It then requires one uniquely ordered complete envelope and
119+
one successful terminal record, verifies every payload/manifest digest and exact
120+
named role, and checks the substantive stage, config, source, tool, command,
121+
ownership, original-case and upstream-JSON evidence. C/R original C objects,
122+
complete commands, package/tool closures and config values must match, except
123+
the one normalizer selector. Full output and symlink inventories must remain
124+
identical through each member's no-op, audit and guest.
125+
126+
This validates the evidence contract for logs obtained from the verified run.
127+
An offline parser cannot authenticate an invented download receipt or distinguish
128+
a fully fabricated, internally consistent report from its purported origin.
129+
The GitHub metadata/download verification is therefore a required trust boundary.
130+
The positive synthetic fixtures intentionally exercise the protocol; they are
131+
never evidence of a kernel build or runtime pass.
132+
133+
Even a complete passing envelope retains observations and hashes, not final ELF
134+
bytes. It remains a provisional logs-only C/R result. Independent later binary
135+
reinspection requires separately approved bounded artifacts; no artifact upload
136+
or binary-through-logs substitute is included here.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: GPL-2.0-only
3+
set -euo pipefail
4+
[[ ${PILOT_RUN_ATTEMPT:-} == 1 ]]
5+
for key in PILOT_JOB_STARTED_EPOCH PILOT_JOB_DEADLINE_EPOCH PILOT_CONTAINER_STARTED_EPOCH PILOT_CONTAINER_DEADLINE_EPOCH; do
6+
[[ ${!key:-} =~ ^[1-9][0-9]{9,11}$ ]] || exit 78
7+
done
8+
now=$(date -u +%s) || exit 79
9+
[[ $now =~ ^[1-9][0-9]{9,11}$ ]] || exit 79
10+
job_remaining=$((PILOT_JOB_DEADLINE_EPOCH-now))
11+
container_remaining=$((PILOT_CONTAINER_DEADLINE_EPOCH-now))
12+
# 17-minute stage covers unchanged QEMU900s+kill10s and110s postcheck margin.
13+
# Parser300s, cleanup60s and evidence180s are reserved in addition to the stage.
14+
required=1560
15+
(( PILOT_JOB_DEADLINE_EPOCH == PILOT_JOB_STARTED_EPOCH+6900 && PILOT_CONTAINER_STARTED_EPOCH >= PILOT_JOB_STARTED_EPOCH && PILOT_CONTAINER_DEADLINE_EPOCH <= PILOT_CONTAINER_STARTED_EPOCH+6000 && PILOT_CONTAINER_DEADLINE_EPOCH <= PILOT_JOB_DEADLINE_EPOCH-180 && now >= PILOT_CONTAINER_STARTED_EPOCH && container_remaining >= required && job_remaining >= required )) || exit 78
16+
printf '{"now":%s,"job_remaining":%s,"container_remaining":%s,"required_seconds":1560,"guest_timeout":900,"guest_kill_grace":10,"guest_stage_allowance":1020,"parser_seconds":300,"cleanup_seconds":60,"evidence_seconds":180}\n' "$now" "$job_remaining" "$container_remaining" || exit 79
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: GPL-2.0-only
3+
set -euo pipefail
4+
here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
5+
cat "$here/launch.json"
6+
# These values may be changed only in the exact owner-reviewed setup revision.
7+
# This is a recorded approval gate, not a way for software to grant permission.
8+
if ! jq -e '.schema==1 and .status=="reviewed-pilot" and .public_source_review=="approved" and .owner_pilot_approval=="one-logs-only-execution-approved" and .evidence_mode=="logs-only"' "$here/launch.json" >/dev/null; then
9+
printf '%s\n' 'LAUNCH_BLOCKED: exact public draft and one logs-only execution lack owner approval.' >&2
10+
exit 78
11+
fi

0 commit comments

Comments
 (0)