From 66c7a22d04e986c02b16910bac33aff84446b4dd Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 8 Oct 2026 03:23:42 -0400 Subject: [PATCH] Recognize verified preserved relay ownership during later updates --- .../managed-update-recovery-implementation.md | 20 +++++++++++++++ scripts/indeehub-maintenance-controller.py | 24 +++++++++++++++--- .../test_indeehub_maintenance_controller.py | 25 +++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/docs/managed-update-recovery-implementation.md b/docs/managed-update-recovery-implementation.md index 2c379d07..db55efd3 100644 --- a/docs/managed-update-recovery-implementation.md +++ b/docs/managed-update-recovery-implementation.md @@ -523,3 +523,23 @@ Source review found byte-download progress unconditionally changes Updating to Installing; failure cleanup only releases Updating. The narrow progress-state fix and regression are pending. This is not full target rollback acceptance. 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. diff --git a/scripts/indeehub-maintenance-controller.py b/scripts/indeehub-maintenance-controller.py index 0cbc5556..304ad875 100644 --- a/scripts/indeehub-maintenance-controller.py +++ b/scripts/indeehub-maintenance-controller.py @@ -86,7 +86,25 @@ elif [ "$1" = signal ]; then else exit 1; fi ''' 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() for _ in range(16): 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(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') - 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())) require(len(matches)==1,'Relay image lacks unique completed owned recovery lineage') image,unit_sha256=matches[0] @@ -356,7 +374,7 @@ class Controller: 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') 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) 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()) diff --git a/tests/regression/test_indeehub_maintenance_controller.py b/tests/regression/test_indeehub_maintenance_controller.py index 1330972f..311d7981 100644 --- a/tests/regression/test_indeehub_maintenance_controller.py +++ b/tests/regression/test_indeehub_maintenance_controller.py @@ -562,6 +562,31 @@ console.log('process identity cases passed');''' if change=='wrong-body':bad['members'][0]['pinned_original_body']='changed' if change=='cycle':bad['members'][0]['original']['image']=image 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): import copy columns=[['first','integer',True,'','',''],['last','text',False,'','','']]