From aed84ceb898434f828c1c3c2907f108e9750c8f1 Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Fri, 6 Nov 2026 01:07:21 -0300 Subject: [PATCH 1/7] fix(remote): preserve BLOB kind across IBatch continuation (#9251) --- src/remote/protocol.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/remote/protocol.cpp b/src/remote/protocol.cpp index 85f17225ba9..99400b42fa8 100644 --- a/src/remote/protocol.cpp +++ b/src/remote/protocol.cpp @@ -2502,7 +2502,8 @@ static bool_t xdr_blob_stream(RemoteXdr* xdrs, SSHORT statement_id, CSTRING* str flow.newBlob(*blobSize, *bpbSize); localStrm.curBpb.clear(); - if (!flow.bpbSize) + // Continuation headers have a zero BLOB ID: retain the previous BLOB type. + if (!flow.bpbSize && (batchBlobId->gds_quad_high || batchBlobId->gds_quad_low)) localStrm.segmented = statement->rsr_batch_flags & (1 << Jrd::DsqlBatch::FLAG_DEFAULT_SEGMENTED); continue; From 3d968120f262599e2049d23d501559c494df8c2d Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Fri, 6 Nov 2026 21:59:20 -0300 Subject: [PATCH 2/7] refactor(remote): isolate zero-allocation IBatch BLOB-kind guard (#9251) --- src/remote/protocol.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/remote/protocol.cpp b/src/remote/protocol.cpp index 99400b42fa8..1dc88f9de97 100644 --- a/src/remote/protocol.cpp +++ b/src/remote/protocol.cpp @@ -27,6 +27,17 @@ * */ +/* + * IBatch BLOB-kind invariant (issue #9251): + * NEW : nonzero BLOB ID + no BPB -> use batch default kind flag. + * BPB : nonzero BLOB ID + BPB -> decode the explicit kind from BPB. + * CONT : zero BLOB ID -> preserve the current BLOB kind. + * + * Build diagnostics: keep compiler warnings visible. They help detect unsafe + * conversions and flag handling; suppressing them cannot fix protocol bugs. + * The guard below adds no dependency, allocation or public ABI change. + */ + #include "firebird.h" #include #include @@ -2355,6 +2366,15 @@ static Rtr* getTransaction(RemoteXdr* xdrs, USHORT tran_id) } } +// One action: apply the default kind only to a new BLOB without an explicit BPB. +// Continuations retain the kind established by the preceding BLOB header. +static inline void applyDefaultBlobKind(Rsr::BatchStream& stream, const ISC_QUAD& blobId, + ULONG bpbSize, ULONG batchFlags) noexcept +{ + if (!bpbSize && (blobId.gds_quad_high || blobId.gds_quad_low)) + stream.segmented = (batchFlags & (1U << Jrd::DsqlBatch::FLAG_DEFAULT_SEGMENTED)) != 0; +} + static bool_t xdr_blob_stream(RemoteXdr* xdrs, SSHORT statement_id, CSTRING* strmPortion) { if (xdrs->x_op == XDR_FREE) @@ -2502,9 +2522,7 @@ static bool_t xdr_blob_stream(RemoteXdr* xdrs, SSHORT statement_id, CSTRING* str flow.newBlob(*blobSize, *bpbSize); localStrm.curBpb.clear(); - // Continuation headers have a zero BLOB ID: retain the previous BLOB type. - if (!flow.bpbSize && (batchBlobId->gds_quad_high || batchBlobId->gds_quad_low)) - localStrm.segmented = statement->rsr_batch_flags & (1 << Jrd::DsqlBatch::FLAG_DEFAULT_SEGMENTED); + applyDefaultBlobKind(localStrm, *batchBlobId, flow.bpbSize, statement->rsr_batch_flags); continue; } From a5853d280291ce56bfc9aa236044f48019d3888f Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Fri, 6 Nov 2026 22:45:20 -0300 Subject: [PATCH 3/7] test(freestanding): introduce isolated audit gate public contract --- .../raf_freestanding_gate/include/raf_gate.h | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 contrib/raf_freestanding_gate/include/raf_gate.h diff --git a/contrib/raf_freestanding_gate/include/raf_gate.h b/contrib/raf_freestanding_gate/include/raf_gate.h new file mode 100644 index 00000000000..d0b8f255ef6 --- /dev/null +++ b/contrib/raf_freestanding_gate/include/raf_gate.h @@ -0,0 +1,71 @@ +/* + * RAFAELIA freestanding evidence gate — independent optional experiment. + * Original contribution offered under the InterBase Public License 1.0. + * Provided AS IS, without warranty; not part of Firebird's public ABI. + * + * Warnings are diagnostics, not successes. Do not suppress them to fake a gate. + * Strict types, explicit bounds, and replayable receipts aid inspection. + */ +#ifndef RAF_FREESTANDING_GATE_H +#define RAF_FREESTANDING_GATE_H +#include +#include + +#define RAF_SHA256_BYTES 32u +#define RAF_RECEIPT_BYTES 114u +#define RAF_RECEIPT_PREFIX_BYTES 82u + +typedef enum raf_state { + RAF_BLOCKED = 0, + RAF_PASS = 1, + RAF_ROLLED_BACK = 2, + RAF_FAILBACK = 3, + RAF_UNRECOVERABLE = 4, + RAF_PREPARED = 16 +} raf_state; + +typedef struct raf_receipt { uint8_t bytes[RAF_RECEIPT_BYTES]; } raf_receipt; +typedef int (*raf_step_fn)(void *context); +/* Return 1 only for atomically durable append, 0 for definite rejection, + * -1 for indeterminate persistence. Never report an unverified success. */ +typedef int (*raf_persist_fn)(void *context, const raf_receipt *receipt); + +typedef struct raf_ports { + void *context; + raf_step_fn preflight; /* Mandatory, read-only */ + raf_step_fn apply; /* Mandatory, reversible and idempotent */ + raf_step_fn verify; /* Mandatory, checks postconditions */ + raf_step_fn rollback; /* Mandatory, checked compensation */ + raf_step_fn failback; /* Optional, switch to independently known-good */ + raf_step_fn verify_recovery; /* Mandatory independent recovery postcheck */ + raf_persist_fn persist;/* Mandatory, CAS chain/sequence at durable store */ +} raf_ports; + +typedef struct raf_request { + uint64_t next_seq; + uint8_t previous_sha256[RAF_SHA256_BYTES]; + const uint8_t *evidence; + size_t evidence_bytes; +} raf_request; + +typedef struct raf_result { + raf_state state; + uint8_t terminal_durable; + raf_receipt prepared; + raf_receipt terminal; +} raf_result; + +/* No libc, allocation, I/O, clock, threads, or platform runtime calls. */ +void raf_sha256(const uint8_t *data, size_t size, uint8_t digest[RAF_SHA256_BYTES]); +int raf_receipt_create(raf_receipt *out, uint64_t seq, raf_state state, + const uint8_t prev_sha256[RAF_SHA256_BYTES], + const uint8_t *evidence, size_t evidence_bytes); +int raf_receipt_valid(const raf_receipt *r, uint64_t expected_seq, + const uint8_t expected_prev_sha256[RAF_SHA256_BYTES]); +int raf_receipt_evidence_match(const raf_receipt *r, const uint8_t *evidence, + size_t evidence_bytes); +void raf_gate_run(raf_result *out, const raf_request *request, const raf_ports *ports); +/* RFC 5424 header/message text profile only; TLS transport is NOT included. */ +int raf_syslog5424_format(char *out, size_t capacity, size_t *written, + const raf_receipt *receipt); +#endif From 2e0d7b1f0bb26fe78566eaf1f3a5241c67bd847c Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Fri, 6 Nov 2026 22:45:58 -0300 Subject: [PATCH 4/7] feat(freestanding): implement deterministic receipts, SHA256, fail-closed gate and syslog profile --- contrib/raf_freestanding_gate/src/raf_gate.c | 182 +++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 contrib/raf_freestanding_gate/src/raf_gate.c diff --git a/contrib/raf_freestanding_gate/src/raf_gate.c b/contrib/raf_freestanding_gate/src/raf_gate.c new file mode 100644 index 00000000000..9da36211dbe --- /dev/null +++ b/contrib/raf_freestanding_gate/src/raf_gate.c @@ -0,0 +1,182 @@ +/* + * RAFAELIA freestanding evidence gate — independent optional experiment. + * Original contribution offered under the InterBase Public License 1.0. + * Provided AS IS, without warranty; not part of Firebird's public ABI. + * + * Compiler warnings must be investigated: an unsafe conversion or omitted + * branch may invalidate a failure gate. No warning is silenced here. + */ +#include "raf_gate.h" + +/* SHA-256 following FIPS 180-4/RFC 6234 algorithm, original implementation. */ +static const uint32_t k256[64] = { + 0x428a2f98u,0x71374491u,0xb5c0fbcfu,0xe9b5dba5u,0x3956c25bu,0x59f111f1u,0x923f82a4u,0xab1c5ed5u, + 0xd807aa98u,0x12835b01u,0x243185beu,0x550c7dc3u,0x72be5d74u,0x80deb1feu,0x9bdc06a7u,0xc19bf174u, + 0xe49b69c1u,0xefbe4786u,0x0fc19dc6u,0x240ca1ccu,0x2de92c6fu,0x4a7484aau,0x5cb0a9dcu,0x76f988dau, + 0x983e5152u,0xa831c66du,0xb00327c8u,0xbf597fc7u,0xc6e00bf3u,0xd5a79147u,0x06ca6351u,0x14292967u, + 0x27b70a85u,0x2e1b2138u,0x4d2c6dfcu,0x53380d13u,0x650a7354u,0x766a0abbu,0x81c2c92eu,0x92722c85u, + 0xa2bfe8a1u,0xa81a664bu,0xc24b8b70u,0xc76c51a3u,0xd192e819u,0xd6990624u,0xf40e3585u,0x106aa070u, + 0x19a4c116u,0x1e376c08u,0x2748774cu,0x34b0bcb5u,0x391c0cb3u,0x4ed8aa4au,0x5b9cca4fu,0x682e6ff3u, + 0x748f82eeu,0x78a5636fu,0x84c87814u,0x8cc70208u,0x90befffau,0xa4506cebu,0xbef9a3f7u,0xc67178f2u +}; +static uint32_t rotate(uint32_t x, unsigned count) { return (x >> count) | (x << (32u-count)); } +static uint32_t get_be32(const uint8_t *p) { + return ((uint32_t)p[0]<<24)|((uint32_t)p[1]<<16)|((uint32_t)p[2]<<8)|(uint32_t)p[3]; +} +static void put_be32(uint8_t *p, uint32_t x) { + p[0]=(uint8_t)(x>>24);p[1]=(uint8_t)(x>>16);p[2]=(uint8_t)(x>>8);p[3]=(uint8_t)x; +} +static void put_be64(uint8_t *p, uint64_t x) { + unsigned i;for(i=0;i<8u;i++)p[i]=(uint8_t)(x>>(56u-8u*i)); +} +static uint64_t get_be64(const uint8_t *p) { + uint64_t x=0; unsigned i;for(i=0;i<8u;i++) x=(x<<8)|(uint64_t)p[i];return x; +} +static int same(const uint8_t *a,const uint8_t *b,size_t n) { + uint8_t d=0;size_t i;for(i=0;iblock[i*4u]); + for(i=16;i<64u;i++){ + uint32_t x=w[i-15u],y=w[i-2u]; + uint32_t s0=rotate(x,7u)^rotate(x,18u)^(x>>3); + uint32_t s1=rotate(y,17u)^rotate(y,19u)^(y>>10); + w[i]=w[i-16u]+s0+w[i-7u]+s1; + } + a=c->h[0];b=c->h[1];cc=c->h[2];d=c->h[3];e=c->h[4];f=c->h[5];g=c->h[6];h=c->h[7]; + for(i=0;i<64u;i++){ + uint32_t s1=rotate(e,6u)^rotate(e,11u)^rotate(e,25u); + uint32_t ch=(e&f)^(~e&g); + uint32_t s0=rotate(a,2u)^rotate(a,13u)^rotate(a,22u); + uint32_t maj=(a&b)^(a&cc)^(b&cc); + t1=h+s1+ch+k256[i]+w[i];t2=s0+maj; + h=g;g=f;f=e;e=d+t1;d=cc;cc=b;b=a;a=t1+t2; + } + c->h[0]+=a;c->h[1]+=b;c->h[2]+=cc;c->h[3]+=d; + c->h[4]+=e;c->h[5]+=f;c->h[6]+=g;c->h[7]+=h; +} +static void sha_init(sha256ctx *c) { + static const uint32_t iv[8]={0x6a09e667u,0xbb67ae85u,0x3c6ef372u,0xa54ff53au, + 0x510e527fu,0x9b05688cu,0x1f83d9abu,0x5be0cd19u}; + unsigned i;for(i=0;i<8u;i++)c->h[i]=iv[i];c->used=0;c->count=0; +} +static void sha_byte(sha256ctx *c,uint8_t v) { + c->block[c->used++]=v;c->count++; + if(c->used==64u){sha_block(c);c->used=0;} +} +void raf_sha256(const uint8_t *data,size_t size,uint8_t digest[RAF_SHA256_BYTES]){ + sha256ctx c;size_t i;unsigned j;uint64_t bits; + if(!digest || (!data && size!=0u) || size>UINT64_MAX/8u) return; + sha_init(&c);for(i=0;i>(56u-8u*j))); + for(j=0;j<8u;j++)put_be32(&digest[j*4u],c.h[j]); +} +static void copy(uint8_t *d,const uint8_t *s,size_t n) {size_t i;for(i=0;iUINT32_MAX||!state_valid(state))return 0; + b=out->bytes;b[0]='R';b[1]='F';b[2]='G';b[3]='1';b[4]=1u; + put_be64(&b[5],seq);b[13]=(uint8_t)state;copy(&b[14],prev,32u); + put_be32(&b[46],(uint32_t)n);raf_sha256(evidence,n,evhash); + copy(&b[50],evhash,32u);raf_sha256(b,RAF_RECEIPT_PREFIX_BYTES,&b[82]);return 1; +} +int raf_receipt_valid(const raf_receipt *r,uint64_t seq,const uint8_t prev[32]){ + uint8_t check[32];const uint8_t *b; + if(!r||!prev)return 0; + b=r->bytes; + if(b[0]!='R'||b[1]!='F'||b[2]!='G'||b[3]!='1'||b[4]!=1u|| + get_be64(&b[5])!=seq||!state_valid((raf_state)b[13])||!same(&b[14],prev,32u))return 0; + raf_sha256(b,RAF_RECEIPT_PREFIX_BYTES,check);return same(check,&b[82],32u); +} +int raf_receipt_evidence_match(const raf_receipt *r,const uint8_t *ev,size_t n){ + uint8_t h[32]; + if(!r||!ev||n==0u||n>UINT32_MAX||get_be32(&r->bytes[46])!=(uint32_t)n)return 0; + raf_sha256(ev,n,h);return same(h,&r->bytes[50],32u); +} +static void initialize(raf_result *out){ + size_t i;out->state=RAF_BLOCKED;out->terminal_durable=0u; + for(i=0;iprepared.bytes[i]=0u;out->terminal.bytes[i]=0u;} +} +static raf_state recover(raf_ports const *ports){ + if(ports->rollback(ports->context)>0 && ports->verify_recovery(ports->context)>0) + return RAF_ROLLED_BACK; + if(ports->failback && ports->failback(ports->context)>0 && + ports->verify_recovery(ports->context)>0)return RAF_FAILBACK; + return RAF_UNRECOVERABLE; +} +void raf_gate_run(raf_result *out,const raf_request *req,const raf_ports *ports){ + uint8_t prev2[32];int persist_status;raf_state s; + if(!out)return; + initialize(out); + if(!req||!ports||!ports->preflight||!ports->apply||!ports->verify|| + !ports->rollback||!ports->verify_recovery||!ports->persist|| + req->next_seq==UINT64_MAX||!req->evidence||req->evidence_bytes==0u|| + req->evidence_bytes>UINT32_MAX)return; + if(ports->preflight(ports->context)<=0){ + if(raf_receipt_create(&out->terminal,req->next_seq,RAF_BLOCKED,req->previous_sha256, + req->evidence,req->evidence_bytes)) + out->terminal_durable=(uint8_t)(ports->persist(ports->context,&out->terminal)>0); + return; + } + if(!raf_receipt_create(&out->prepared,req->next_seq,RAF_PREPARED,req->previous_sha256, + req->evidence,req->evidence_bytes))return; + persist_status=ports->persist(ports->context,&out->prepared); + if(persist_status!=1)return; /* no execution after missing/uncertain prepare receipt */ + copy(prev2,&out->prepared.bytes[82],32u); + s=(ports->apply(ports->context)>0 && ports->verify(ports->context)>0)?RAF_PASS:recover(ports); + if(!raf_receipt_create(&out->terminal,req->next_seq+1u,s,prev2, + req->evidence,req->evidence_bytes)){out->state=RAF_UNRECOVERABLE;return;} + persist_status=ports->persist(ports->context,&out->terminal); + if(persist_status==1){out->state=s;out->terminal_durable=1u;return;} + if(persist_status<0){out->state=RAF_UNRECOVERABLE;return;} /* unknown outcome: reconcile! */ + if(s==RAF_PASS){ + s=recover(ports); + if(!raf_receipt_create(&out->terminal,req->next_seq+1u,s,prev2, + req->evidence,req->evidence_bytes)){out->state=RAF_UNRECOVERABLE;return;} + if(ports->persist(ports->context,&out->terminal)==1){ + out->state=s;out->terminal_durable=1u;return; + } + } + out->state=RAF_UNRECOVERABLE; /* never claim safe recovery without receipt */ +} +static int append(char *out,size_t cap,size_t *at,const char *s){ + size_t n=0u,i;while(s[n])n++; + if(*at>cap||n>=cap-*at)return 0; + for(i=0;icap||i>=cap-*at)return 0; + while(i)out[(*at)++]=temp[--i]; + out[*at]='\0';return 1; +} +int raf_syslog5424_format(char *out,size_t capacity,size_t *written,const raf_receipt *receipt){ + static const char hex[]="0123456789abcdef";size_t at=0,i;const uint8_t *b; + if(written)*written=0; + if(!out||capacity==0u)return 0; + out[0]='\0'; + if(!receipt||!raf_receipt_valid(receipt,get_be64(&receipt->bytes[5]),&receipt->bytes[14]))return 0; + b=receipt->bytes; + if(!append(out,capacity,&at,"<134>1 - - RAFAELIA - AUDIT - seq=")|| + !dec(out,capacity,&at,get_be64(&b[5]))|| + !append(out,capacity,&at," state=")|| + !dec(out,capacity,&at,(uint64_t)b[13])|| + !append(out,capacity,&at," sha256="))goto fail; + if(at>capacity||64u>=capacity-at)goto fail; + for(i=0;i<32u;i++){out[at++]=hex[b[82u+i]>>4];out[at++]=hex[b[82u+i]&0x0fu];} + out[at]='\0';if(written)*written=at;return 1; +fail:out[0]='\0';return 0; +} From d769bc4aba99f4a74b7b3e725f96c11807288324 Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Fri, 6 Nov 2026 22:46:23 -0300 Subject: [PATCH 5/7] test(freestanding): cover SHA, receipt chain, syslog bounds and adversarial failback cases --- .../tests/test_raf_gate.c | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 contrib/raf_freestanding_gate/tests/test_raf_gate.c diff --git a/contrib/raf_freestanding_gate/tests/test_raf_gate.c b/contrib/raf_freestanding_gate/tests/test_raf_gate.c new file mode 100644 index 00000000000..26b3ae0d998 --- /dev/null +++ b/contrib/raf_freestanding_gate/tests/test_raf_gate.c @@ -0,0 +1,77 @@ +/* Host-only test harness; core remains freestanding. */ +#include "raf_gate.h" +#include +#include +#include + +typedef struct fixture { + int pre,apply,verify,rollback,failback,recovery,deny_persist_at,uncertain_at; + unsigned calls,applied,verified,rollbacks,failbacks,recovery_checks,persisted; + raf_receipt journal[3]; +} fixture; +static int pre(void *v){return ((fixture*)v)->pre;} +static int app(void *v){fixture*f=v;f->applied++;return f->apply;} +static int ver(void *v){fixture*f=v;f->verified++;return f->verify;} +static int roll(void *v){fixture*f=v;f->rollbacks++;return f->rollback;} +static int back(void *v){fixture*f=v;f->failbacks++;return f->failback;} +static int recover_check(void *v){fixture*f=v;f->recovery_checks++;return f->recovery;} +static int store(void *v,const raf_receipt*r){ + fixture*f=v;f->calls++; + if((int)f->calls==f->uncertain_at)return -1; + if((int)f->calls==f->deny_persist_at)return 0; + assert(f->persisted<3u);f->journal[f->persisted++]=*r;return 1; +} +static void setup(fixture*f,raf_ports*p,raf_request*request){ + static const uint8_t ev[4]={'T','E','S','T'}; + memset(f,0,sizeof(*f));f->pre=f->apply=f->verify=f->rollback=f->failback=f->recovery=1; + p->context=f;p->preflight=pre;p->apply=app;p->verify=ver;p->rollback=roll; + p->failback=back;p->verify_recovery=recover_check;p->persist=store; + memset(request,0,sizeof(*request));request->next_seq=42u; + request->evidence=ev;request->evidence_bytes=4u; +} +static void verify_links(fixture*f,raf_request*r){ + assert(f->persisted==2u); + assert(raf_receipt_valid(&f->journal[0],42u,r->previous_sha256)); + assert(raf_receipt_valid(&f->journal[1],43u,&f->journal[0].bytes[82])); +} +static void test_sha(void){ + const uint8_t abc[3]={'a','b','c'};uint8_t h[32];char b[65];size_t i; + static const char *gold="ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"; + static const char *empty="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + static const char hexd[]="0123456789abcdef"; + raf_sha256(abc,3u,h);for(i=0;i<32u;i++){b[2u*i]=hexd[h[i]>>4];b[2u*i+1u]=hexd[h[i]&15u];}b[64]=0; + assert(strcmp(b,gold)==0); + raf_sha256(NULL,0,h);for(i=0;i<32u;i++){b[2u*i]=hexd[h[i]>>4];b[2u*i+1u]=hexd[h[i]&15u];}assert(strcmp(b,empty)==0); +} +int main(void){ + fixture f;raf_ports p;raf_request q;raf_result r;size_t length=0;char sys[180];int tests=0; + test_sha();tests++; + setup(&f,&p,&q);raf_gate_run(&r,&q,&p); + assert(raf_receipt_evidence_match(&r.terminal,q.evidence,q.evidence_bytes)); + {const uint8_t bad[4]={'B','A','D','!'}; + assert(!raf_receipt_evidence_match(&r.terminal,bad,sizeof(bad)));}tests++; + setup(&f,&p,&q);raf_gate_run(&r,&q,&p);assert(r.state==RAF_PASS&&r.terminal_durable&&f.applied==1u);verify_links(&f,&q);tests++; + assert(raf_syslog5424_format(sys,sizeof(sys),&length,&r.terminal)&&length==strlen(sys)); + assert(strncmp(sys,"<134>1 - - RAFAELIA - AUDIT - seq=43",strlen("<134>1 - - RAFAELIA - AUDIT - seq=43"))==0);tests++; + sys[0]='X';assert(!raf_syslog5424_format(sys,10u,&length,&r.terminal)&&sys[0]==0);tests++; + r.terminal.bytes[56]^=1u;assert(!raf_receipt_valid(&r.terminal,43u,&f.journal[0].bytes[82]));tests++; + setup(&f,&p,&q);f.pre=0;raf_gate_run(&r,&q,&p);assert(r.state==RAF_BLOCKED&&f.applied==0u&&f.persisted==1u);tests++; + setup(&f,&p,&q);f.deny_persist_at=1;raf_gate_run(&r,&q,&p);assert(r.state==RAF_BLOCKED&&f.applied==0u&&f.persisted==0u);tests++; + setup(&f,&p,&q);f.uncertain_at=1;raf_gate_run(&r,&q,&p);assert(r.state==RAF_BLOCKED&&f.applied==0u);tests++; + setup(&f,&p,&q);f.apply=0;raf_gate_run(&r,&q,&p);assert(r.state==RAF_ROLLED_BACK&&f.rollbacks==1u&&f.recovery_checks==1u&&f.verified==0u);verify_links(&f,&q);tests++; + setup(&f,&p,&q);f.verify=0;raf_gate_run(&r,&q,&p);assert(r.state==RAF_ROLLED_BACK&&f.rollbacks==1u&&f.verified==1u);tests++; + setup(&f,&p,&q);f.verify=0;f.rollback=0;raf_gate_run(&r,&q,&p);assert(r.state==RAF_FAILBACK&&f.failbacks==1u&&r.terminal_durable);tests++; + setup(&f,&p,&q);f.verify=0;f.rollback=0;f.failback=0;raf_gate_run(&r,&q,&p);assert(r.state==RAF_UNRECOVERABLE&&r.terminal_durable);tests++; + setup(&f,&p,&q);f.verify=0;f.recovery=0;raf_gate_run(&r,&q,&p); + assert(r.state==RAF_UNRECOVERABLE&&f.rollbacks==1u&&f.failbacks==1u&&f.recovery_checks==2u);tests++; + setup(&f,&p,&q);f.deny_persist_at=2;raf_gate_run(&r,&q,&p);assert(r.state==RAF_ROLLED_BACK&&f.rollbacks==1u&&f.persisted==2u);verify_links(&f,&q);tests++; + setup(&f,&p,&q);f.uncertain_at=2;raf_gate_run(&r,&q,&p);assert(r.state==RAF_UNRECOVERABLE&&f.rollbacks==0u&&!r.terminal_durable);tests++; + setup(&f,&p,&q);q.next_seq=UINT64_MAX;raf_gate_run(&r,&q,&p);assert(r.state==RAF_BLOCKED&&f.calls==0u);tests++; + setup(&f,&p,&q);p.verify_recovery=NULL;raf_gate_run(&r,&q,&p); + assert(r.state==RAF_BLOCKED&&f.calls==0u);tests++; + setup(&f,&p,&q);p.rollback=NULL;raf_gate_run(&r,&q,&p);assert(r.state==RAF_BLOCKED&&f.calls==0u);tests++; + setup(&f,&p,&q);q.evidence=NULL;raf_gate_run(&r,&q,&p);assert(r.state==RAF_BLOCKED&&f.calls==0u);tests++; + setup(&f,&p,&q);q.evidence_bytes=0u;raf_gate_run(&r,&q,&p); + assert(r.state==RAF_BLOCKED&&f.calls==0u);tests++; + printf("PASS_SCOPED_HOST: %d tests\n",tests);return 0; +} From be3ec881a98c21d6940c195018fb28f0199231e3 Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Fri, 6 Nov 2026 22:47:25 -0300 Subject: [PATCH 6/7] docs(freestanding): define threat model, standards gap matrix, DMAIC and rollback contracts --- contrib/raf_freestanding_gate/README.md | 176 ++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 contrib/raf_freestanding_gate/README.md diff --git a/contrib/raf_freestanding_gate/README.md b/contrib/raf_freestanding_gate/README.md new file mode 100644 index 00000000000..954fd63d45a --- /dev/null +++ b/contrib/raf_freestanding_gate/README.md @@ -0,0 +1,176 @@ +# Freestanding evidence gate V1 — Firebird #9251 experimental companion + +> Scope: **experimental, isolated and opt-in**, not part of the upstream fix, +> not a Firebird server plugin, not a certified standards implementation. +> The producer fix stays in [PR #9274](https://github.com/FirebirdSQL/firebird/pull/9274). +> Firebird 4/5 backports remain separate. + +## Boundary and portability + +- Core language: C11, `stddef.h` + `stdint.h`; one .c and one .h. +- No allocation, file/network calls, syscalls, globals with mutable state, + libc calls, exceptions, threading or host clock. +- All state changes and durable writes happen **outside** the core through + explicit callbacks. The host test harness does use libc; the **core does not**. +- Source files are offered under the InterBase Public License 1.0, preserving + the Firebird repository's existing licensing boundary. No external code + or third-party cryptographic library is copied into the core. +- No change to `src/remote/protocol.cpp`, existing Firebird build rules, ABI, + production logging or repository release branches in this experiment. + +## Contract / typed states + +``` +request + nonempty evidence + parent digest + next sequence + | + v +preflight (read-only) ----fail----> BLOCKED receipt (if persisted) + | + v +atomic PREPARED receipt ----not definitely durable----> BLOCKED, no apply + | + v +apply -> verify ----------------success----> PASS terminal receipt + | | + | failure terminal persist definitely rejected + v | +rollback -> verify_recovery <--------------------+ + | success + v +ROLLED_BACK receipt + | failure + v +failback -> verify_recovery -> FAILBACK receipt or UNRECOVERABLE +``` + +Return codes from `persist`: **1** = confirmed durable, **0** = definitely +not appended, **-1** = indeterminate. An indeterminate terminal append +sets `UNRECOVERABLE` pending human/host reconciliation; it is NOT a +declaration of physical machine damage. Under an uncertain outcome, the +core does not guess which state was stored or silently perform a second +potentially conflicting action. + +**Mandatory adapter duties:** + +1. Authorize the change and prove an independent known-good rollback state + before enabling `apply`; preflight MUST have no side effect. +2. `apply` MUST be reversible and idempotent. `rollback` and `failback` + MUST be idempotent and implement distinct recovery strategies. Do not + wrap irreversible production writes behind this API. +3. `verify_recovery` MUST check the **actual recovered/fallback state**, + not just repeat the recovery callback's return code. +4. `persist` MUST atomically compare-and-swap (sequence, previous hash) + at a durable append-only store; return 1 only after a readback-equivalent + durability guarantee. A callback that always returns 1 is a TEST STUB, + not a production adapter. +5. On crash after PREPARED, the host MUST reconcile in-flight effects and + emit an append-only successor. The core does not provide process restart, + power-loss recovery, concurrency control, or a watchdog. +6. The host MUST supply attributable, privacy-reviewed evidence bytes and + retain them securely. A digest without archived original bytes cannot + prove the meaning or authenticity of the evidence. +7. A SHA-256 chain detects accidental or unauthorized changes only when + compared with a trusted anchor. It is **not a digital signature**, identity + attestation, secure timestamp, or complete intrusion defense. +8. Never let a gate approve a deployment based solely on a host-level test. + +## Receipt wire V1 (114 bytes, big-endian) + +| Offset | Bytes | Field | +|---|---:|---| +| 0 | 4 | ASCII `RFG1` | +| 4 | 1 | format version = 1 | +| 5 | 8 | sequence | +| 13 | 1 | state | +| 14 | 32 | previous record SHA-256 | +| 46 | 4 | exact evidence byte count | +| 50 | 32 | SHA-256 of exact evidence bytes | +| 82 | 32 | SHA-256 of bytes [0,82) | + +A successful transaction produces PREPARED + terminal records, linked by +the previous-digest field. `raf_receipt_valid` checks record integrity and +chain pointer; `raf_receipt_evidence_match` checks independently supplied +evidence against the stored digest. Internal format is a **custom versioned +binary schema**, not CBOR, JSON, RFC 8785 JCS or W3C PROV serialization. + +## Standards mapping: target vs evidence + +| Standard / framework | Applicable boundary | V1 result | +|---|---|---| +| RFC 6234 / SHA-256 | digest test vectors | Two known vectors + independent host tests; no FIPS certification | +| RFC 5424 | syslog header/message syntax | Bounded ASCII text formatter, using NILVALUE for missing clock/host; **NOT** complete syslog service | +| RFC 5425 (and updates) | secure syslog transport | **NOT IMPLEMENTED** — TLS is a host adapter responsibility | +| RFC 3339 | Internet timestamps | **NOT IMPLEMENTED** — host must supply verified time where available | +| RFC 8785 | canonical JSON | **NOT IMPLEMENTED** — receipts use binary schema | +| W3C PROV-O / PROV-DM | entities, activities, agent, derived evidence | **MAPPING PROPOSED**, exporter not implemented | +| IEEE 1012-2024 | V&V process traceability | Design intent only; formal V&V conformance not assessed | +| ISO/IEC/IEEE 29119-2:2021 | governed testing processes | Local adversarial cases, not a process conformance claim | +| 6 Sigma / DMAIC | improvement workflow | Method only; no DPMO measurement, capability index or certification | + +Standards: +[RFC 5424](https://www.rfc-editor.org/rfc/rfc5424), +[RFC 5425](https://www.rfc-editor.org/rfc/rfc5425), +[RFC 6234](https://www.rfc-editor.org/rfc/rfc6234), +[RFC 3339](https://www.rfc-editor.org/rfc/rfc3339), +[RFC 8785](https://www.rfc-editor.org/rfc/rfc8785), +[W3C PROV-O](https://www.w3.org/TR/prov-o/), +[IEEE 1012](https://standards.ieee.org/ieee/1012/7324/), +[ISO/IEC/IEEE 29119-2](https://standards.ieee.org/ieee/2416/7498/). + +A future PROV adapter can map record -> `prov:Entity`, verification +step -> `prov:Activity`, verified external authority -> `prov:Agent`, +and evidence lineage -> `prov:wasDerivedFrom`. No agent identity is +invented by the core. + +## DMAIC and adversarial falsification + +- **Define / ψ:** precise invariant and risk; for Firebird #9251, + continuation with zero BLOB ID MUST preserve the previously decoded kind. +- **Measure / χ:** source commit, current HEAD, failed packet-size case, + host capabilities and baseline. +- **Analyze / ρ:** model corruption, missing BPB, altered default flags, + ambiguous callbacks, timeouts, lost receipts and crash windows. +- **Improve / Δ:** smallest reversible change; freestanding gate stays + separate from the protocol hotfix. +- **Control / Σ → Ω → ψ':** run adverse cases, verify receipt digests, read + provider CI, freeze on uncertainty, and append superseding evidence. + +The 21 C host cases include SHA-256 known vectors, chain-link validation, +evidence substitution, syslog size bounds, invalid/preflight-blocked input, +missing/indeterminate persistence, failed apply, failed verify, rollback, +independently verified failback, failed recovery verification, missing +callbacks and sequence overflow. + +### Reproduce on host (tests only; not a production Firebird build) + +```sh +cd contrib/raf_freestanding_gate +cc -std=c11 -O2 -ffreestanding -fno-builtin -Wall -Wextra -Wpedantic \ + -Wconversion -Wsign-conversion -Werror -Iinclude -c src/raf_gate.c -o /tmp/raf_gate.o +nm -u /tmp/raf_gate.o # expected: no undefined symbols +cc -std=c11 -O1 -g -Wall -Wextra -Wpedantic -Wconversion \ + -Wsign-conversion -Werror -fsanitize=address,undefined \ + -fno-omit-frame-pointer -Iinclude src/raf_gate.c tests/test_raf_gate.c \ + -o /tmp/test_raf_gate +/tmp/test_raf_gate +``` + +Locally scoped: GCC 14.2 and Clang 17 both compiled the isolated +freestanding object with zero undefined symbols and passed 21 host cases +under ASan/UBSan. This is **PASS_SCOPED_HOST**, not Firebird CI or E2E. + +## Integration gate / rollback policy + +This experiment MUST NOT be copied into Firebird's master PR as a broad +unreviewed refactor. A proposed adapter for `src/remote/protocol.cpp` +requires maintainer permission, a separately reviewed architecture, +a scope-specific ABI compatibility check, packet-boundary BLOB regression +tests on Firebird 4/5/master, actual CI receipts, and a documented +runtime rollback/failback procedure. Until then: + +```text +SOURCE != ARTIFACT != EXECUTION != EVIDENCE != CLAIM +CLAIM_ALLOWED=false +UPSTREAM_INTEGRATION=TOKEN_VAZIO +RELEASE_ALLOWED=false +``` From 207e553bfcff04ac730482a0fd99aa95028e6958 Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Fri, 6 Nov 2026 22:47:49 -0300 Subject: [PATCH 7/7] ci(freestanding): scoped warning-clean GCC/Clang builds and adversarial host tests --- .github/workflows/raf-freestanding-gate.yml | 56 +++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/raf-freestanding-gate.yml diff --git a/.github/workflows/raf-freestanding-gate.yml b/.github/workflows/raf-freestanding-gate.yml new file mode 100644 index 00000000000..d39297e6e3e --- /dev/null +++ b/.github/workflows/raf-freestanding-gate.yml @@ -0,0 +1,56 @@ +# Isolated host-only experimental gate; never deploy, merge or release. +name: RAFAELIA freestanding evidence gate (scoped) +on: + pull_request: + paths: + - 'contrib/raf_freestanding_gate/**' + - '.github/workflows/raf-freestanding-gate.yml' + push: + branches: + - research/9251-freestanding-audit-gate-v1 + paths: + - 'contrib/raf_freestanding_gate/**' + - '.github/workflows/raf-freestanding-gate.yml' + +permissions: + contents: read + +jobs: + adversarial-host-scoped: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + cc: [gcc, clang] + steps: + - name: Checkout pinned + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Compile freestanding core, zero external symbols + shell: bash + env: + CC: ${{ matrix.cc }} + run: | + set -euo pipefail + cd contrib/raf_freestanding_gate + "${CC}" -std=c11 -O2 -ffreestanding -fno-builtin \ + -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Werror \ + -Iinclude -c src/raf_gate.c -o "${RUNNER_TEMP}/raf_gate.o" + if [[ -n "$(nm -u "${RUNNER_TEMP}/raf_gate.o")" ]]; then + echo "Core has forbidden undefined symbols"; exit 1 + fi + - name: Adversarial host tests (not Firebird E2E) + shell: bash + env: + CC: ${{ matrix.cc }} + run: | + set -euo pipefail + cd contrib/raf_freestanding_gate + "${CC}" -std=c11 -O1 -g -Wall -Wextra -Wpedantic \ + -Wconversion -Wsign-conversion -Werror \ + -fsanitize=address,undefined -fno-omit-frame-pointer \ + -Iinclude src/raf_gate.c tests/test_raf_gate.c \ + -o "${RUNNER_TEMP}/test_raf_gate" + "${RUNNER_TEMP}/test_raf_gate"