Recognize verified preserved relay ownership during later updates

This commit is contained in:
archipelago
2026-10-08 03:23:42 -04:00
parent 361ba45fe8
commit 66c7a22d04
3 changed files with 66 additions and 3 deletions
@@ -523,3 +523,23 @@ Source review found byte-download progress unconditionally changes Updating to
Installing; failure cleanup only releases Updating. The narrow progress-state Installing; failure cleanup only releases Updating. The narrow progress-state
fix and regression are pending. This is not full target rollback acceptance. fix and regression are pending. This is not full target rollback acceptance.
The recovered guest is QMP-paused without reboot for serialized validation. The recovered guest is QMP-paused without reboot for serialized validation.
The progress-state fix at 105454bd passed the full isolated suite: **2,026
tests**, zero failures, five existing ignores, all 532 inputs unchanged. Its
matching executable retained all seven IDs across manager startup. Actual RPC
`2d4c8fc9-ee90-4167-a2d3-90647e756df0` safely restored before target startup
and **returned package state to Running with progress cleared**, accepting the
state fix on the actual manager path.
That transaction exposed a preserved-relay ownership edge: native pre-target
recovery publishes the latest operation as installed-recipe owner even when the
relay container/image is preserved. The helper incorrectly required that owner
to be the historical image-producing operation. The correction separately
validates a unique terminal schema-2 preservation owner against exact running
intent, live container ID, raw configuration hash, image and unit body, then
retains the existing unique image ancestry to the qualified original. It adds
no image edge or binary-only fallback. **59 controller tests pass**, and a
separate candidate verifier passed against the actual preserved guest relay and
record chain without replacing the installed helper or modifying journals.
Matching embedded-helper build and full target rollback/cutover remain required;
the 2,026-test receipt predates this helper-only correction.
+21 -3
View File
@@ -86,7 +86,25 @@ elif [ "$1" = signal ]; then
else exit 1; fi else exit 1; fi
''' '''
LEGACY_RELAY_IMAGE = '061516573b143b44e331f960036a6a3dc43c9b256ef8ca71afedbeb2cf797a4b' LEGACY_RELAY_IMAGE = '061516573b143b44e331f960036a6a3dc43c9b256ef8ca71afedbeb2cf797a4b'
def verify_relay_image_lineage(image, unit_sha256, records, installed=None): def verify_relay_image_lineage(image, unit_sha256, records, installed=None, current=None):
preserved_owner=False
if image.removeprefix('sha256:')!=LEGACY_RELAY_IMAGE:
require(isinstance(installed,dict) and installed.get('schema')==1 and installed.get('name')=='indeedhub-relay','Relay installed recipe missing')
owners=[r for r in records if r.get('id')==installed.get('operation')]
require(len(owners)==1,'Relay installed owner is missing or ambiguous')
owner=owners[0]
try:require(str(uuid.UUID(owner['id']))==owner['id'],'Invalid relay installed operation')
except (KeyError,ValueError,AttributeError):raise RuntimeError('Invalid relay installed operation')
require(owner.get('schema') in (1,2) and owner.get('package')=='indeedhub' and owner.get('phase')=='Restored' and owner.get('cleanup_done') is True,'Relay installed owner is not a completed recovery')
members=[m for m in owner.get('members',[]) if m.get('original',{}).get('name')=='indeedhub-relay']
require(len(members)==1,'Relay installed owner has ambiguous members')
member=members[0];original=member['original']
if member.get('preserve_original') is True:
require(owner['schema']==2 and owner.get('target_startup_began') is False and original.get('running') is True,'Relay preservation ownership changed')
require(isinstance(current,dict) and current.get('name')=='indeedhub-relay' and original.get('container_id')==current.get('container_id') and original.get('config_sha256')==current.get('config_sha256'),'Relay preserved live identity changed')
require(re.fullmatch('[0-9a-f]{64}',original.get('container_id','')) is not None and re.fullmatch('[0-9a-f]{64}',original.get('config_sha256','')) is not None,'Invalid preserved relay identity')
require(original.get('image','').removeprefix('sha256:')==image.removeprefix('sha256:') and installed.get('body')==original.get('body') and isinstance(original.get('body'),str) and hashlib.sha256(original['body'].encode()).hexdigest()==unit_sha256,'Relay preserved image or recipe changed')
preserved_owner=True
seen=set() seen=set()
for _ in range(16): for _ in range(16):
image=image.removeprefix('sha256:') image=image.removeprefix('sha256:')
@@ -107,7 +125,7 @@ def verify_relay_image_lineage(image, unit_sha256, records, installed=None):
require((record['schema']==1 and (member.get('preserve_original') is None or member.get('preserve_original') is False) or record['schema']==2 and (record.get('target_startup_began') is True and member.get('preserve_original') is None or record.get('target_startup_began') is False and member.get('preserve_original') is False)) and recovery.get('operation_id')==record['id'] and recovery.get('source_container_id')==original.get('container_id'),'Relay recovery ownership changed') require((record['schema']==1 and (member.get('preserve_original') is None or member.get('preserve_original') is False) or record['schema']==2 and (record.get('target_startup_began') is True and member.get('preserve_original') is None or record.get('target_startup_began') is False and member.get('preserve_original') is False)) and recovery.get('operation_id')==record['id'] and recovery.get('source_container_id')==original.get('container_id'),'Relay recovery ownership changed')
require(hashlib.sha256(member['pinned_original_body'].encode()).hexdigest()==unit_sha256,'Relay recovery unit lineage changed') require(hashlib.sha256(member['pinned_original_body'].encode()).hexdigest()==unit_sha256,'Relay recovery unit lineage changed')
require(re.fullmatch('[0-9a-f]{64}',original.get('container_id','')) is not None,'Invalid relay source identity') require(re.fullmatch('[0-9a-f]{64}',original.get('container_id','')) is not None,'Invalid relay source identity')
if len(seen)==1:require(isinstance(installed,dict) and installed.get('schema')==1 and installed.get('name')=='indeedhub-relay' and installed.get('operation')==record['id'] and installed.get('body')==member['pinned_original_body'],'Relay installed recipe does not own recovery lineage') if len(seen)==1:require(preserved_owner and installed['operation']!=record['id'] or installed.get('operation')==record['id'] and installed.get('body')==member['pinned_original_body'],'Relay installed recipe does not own recovery lineage')
matches.append((original['image'],hashlib.sha256(original['body'].encode()).hexdigest())) matches.append((original['image'],hashlib.sha256(original['body'].encode()).hexdigest()))
require(len(matches)==1,'Relay image lacks unique completed owned recovery lineage') require(len(matches)==1,'Relay image lacks unique completed owned recovery lineage')
image,unit_sha256=matches[0] image,unit_sha256=matches[0]
@@ -356,7 +374,7 @@ class Controller:
directory=self.data/'update-transactions'/'installed-units';path=directory/'indeedhub-relay.json' directory=self.data/'update-transactions'/'installed-units';path=directory/'indeedhub-relay.json'
require(directory.is_dir() and not directory.is_symlink() and directory.stat().st_uid==os.getuid() and directory.stat().st_mode & 0o077==0 and path.is_file() and not path.is_symlink() and path.stat().st_uid==os.getuid() and path.stat().st_mode & 0o077==0 and path.stat().st_size<=1024*1024,'Unsafe relay installed recipe') require(directory.is_dir() and not directory.is_symlink() and directory.stat().st_uid==os.getuid() and directory.stat().st_mode & 0o077==0 and path.is_file() and not path.is_symlink() and path.stat().st_uid==os.getuid() and path.stat().st_mode & 0o077==0 and path.stat().st_size<=1024*1024,'Unsafe relay installed recipe')
installed=json.loads(path.read_text()) installed=json.loads(path.read_text())
verify_relay_image_lineage(member['image_id'],member['unit_sha256'],records,installed) verify_relay_image_lineage(member['image_id'],member['unit_sha256'],records,installed,member)
image=images[0];expected=(LEGACY_API_CMD,['docker-entrypoint.sh']) if role=='api' else (LEGACY_RELAY_CMD,None) image=images[0];expected=(LEGACY_API_CMD,['docker-entrypoint.sh']) if role=='api' else (LEGACY_RELAY_CMD,None)
require((image['Config'].get('Cmd'),image['Config'].get('Entrypoint'))==expected,'Unrecognized legacy signal command') require((image['Config'].get('Cmd'),image['Config'].get('Entrypoint'))==expected,'Unrecognized legacy signal command')
source=pathlib.Path(self.run(['systemctl','--user','show',member['name']+'.service','--property=SourcePath','--value']).decode().strip()) source=pathlib.Path(self.run(['systemctl','--user','show',member['name']+'.service','--property=SourcePath','--value']).decode().strip())
@@ -562,6 +562,31 @@ console.log('process identity cases passed');'''
if change=='wrong-body':bad['members'][0]['pinned_original_body']='changed' if change=='wrong-body':bad['members'][0]['pinned_original_body']='changed'
if change=='cycle':bad['members'][0]['original']['image']=image if change=='cycle':bad['members'][0]['original']['image']=image
with self.assertRaises(RuntimeError):module.verify_relay_image_lineage(image,digest,[bad],installed) with self.assertRaises(RuntimeError):module.verify_relay_image_lineage(image,digest,[bad],installed)
def test_preserved_relay_owner_attests_identity_without_replacing_image_ancestry(self):
import copy,hashlib
image='d'*64;body='[Container]\nImage='+image+'\n';digest=hashlib.sha256(body.encode()).hexdigest()
producer={'schema':2,'id':self.operation,'package':'indeedhub','phase':'Restored','cleanup_done':True,'target_startup_began':False,'members':[{'original':{'name':'indeedhub-relay','container_id':'e'*64,'image':module.LEGACY_RELAY_IMAGE,'body':'base'},'pinned_original_body':body,'preserve_original':False,'recovery_image':{'image':image,'operation_id':self.operation,'source_container_id':'e'*64}}]}
current={'name':'indeedhub-relay','container_id':'c'*64,'config_sha256':'a'*64}
owner={'schema':2,'id':str(uuid.uuid4()),'package':'indeedhub','phase':'Restored','cleanup_done':True,'target_startup_began':False,'members':[{'original':dict(current,image=image,body=body,running=True),'preserve_original':True,'recovery_image':{'image':'f'*64}}]}
installed={'schema':1,'name':'indeedhub-relay','operation':owner['id'],'body':body}
module.verify_relay_image_lineage(image,digest,[producer,owner],installed,current)
newer=copy.deepcopy(owner);newer['id']=str(uuid.uuid4());newer['members'][0]['recovery_image']['image']='b'*64
module.verify_relay_image_lineage(image,digest,[producer,owner,newer],dict(installed,operation=newer['id']),current)
for records in ([owner],[producer],[producer,owner,owner]):
with self.assertRaises(RuntimeError):module.verify_relay_image_lineage(image,digest,records,installed,current)
for key,value in [('container_id','b'*64),('config_sha256','b'*64),('name','indeedhub-api')]:
with self.assertRaises(RuntimeError):module.verify_relay_image_lineage(image,digest,[producer,owner],installed,dict(current,**{key:value}))
for field,value in [('schema',1),('target_startup_began',True),('target_startup_began',0),('cleanup_done',False),('phase','Restoring')]:
bad=copy.deepcopy(owner);bad[field]=value
with self.assertRaises(RuntimeError):module.verify_relay_image_lineage(image,digest,[producer,bad],installed,current)
for field,value in [('preserve_original',False),('preserve_original',1),('preserve_original',None)]:
bad=copy.deepcopy(owner);bad['members'][0][field]=value
with self.assertRaises(RuntimeError):module.verify_relay_image_lineage(image,digest,[producer,bad],installed,current)
for field,value in [('image','b'*64),('body','changed'),('running',False),('running',1),('container_id','bad'),('config_sha256','bad')]:
bad=copy.deepcopy(owner);bad['members'][0]['original'][field]=value
with self.assertRaises(RuntimeError):module.verify_relay_image_lineage(image,digest,[producer,bad],installed,current)
with self.assertRaises(RuntimeError):module.verify_relay_image_lineage(image,digest,[producer,owner],dict(installed,body='changed'),current)
with self.assertRaises(RuntimeError):module.verify_relay_image_lineage('b'*64,digest,[producer,owner],installed,current)
def test_column_commitments_retain_logical_order_and_every_semantic_field(self): def test_column_commitments_retain_logical_order_and_every_semantic_field(self):
import copy import copy
columns=[['first','integer',True,'','',''],['last','text',False,'','','']] columns=[['first','integer',True,'','',''],['last','text',False,'','','']]