VulpesOS/tundra/runtime/hal_policy.py
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

98 lines
4.5 KiB
Python

# SPDX-License-Identifier: MPL-2.0
"""Reduce Android init to display HAL services for the read-only Sargo trial.
Input files retain their upstream copyright notices. Removed commands and services
are reported; this is not a general Android init parser or a production policy.
"""
import shlex
SERVICES = frozenset(('ueventd','logd','servicemanager','hwservicemanager','vndservicemanager',
'vendor.hwcomposer-2-2','vendor.qti.hardware.display.allocator','vendor.configstore-hal',
'vendor.power-hal-1-3','vendor.thermal-engine','thermal-hal-1-1','hidl_memory'))
COMMANDS = frozenset(('mkdir','chmod','chown','write','setprop','symlink','copy',
'restorecon','restorecon_recursive','trigger','class_start','class_stop',
'load_system_props','load_persist_props','export','setrlimit','hostname','domainname','loglevel','sysclktz','start','stop','restart'))
PSEUDO_FS = frozenset(('cgroup','cgroup2','cpuset','tmpfs','proc','sysfs','debugfs','pstore','bpf'))
# The native network trial supplies rmt_storage with RAM copies of NV files.
# It retains the blanket denial of block-device access for this container.
NETWORK_SERVICES = frozenset(('irsc_util', 'vendor.per_mgr', 'vendor.rmt_storage',
'vendor.tftp_server', 'cnss-daemon', 'pd_mapper'))
HARDWARE_SERVICES = frozenset(('vendor.qcrild', 'vendor.google.radioext@1.0',
'vendor.audio-hal-2-0', 'vendor.camera-provider-2-4',
'camera_service', 'minimedia', 'minisf', 'miniaf'))
def is_init_script(path):
# ueventd uses a different grammar: these files define device permissions.
return not path.name.startswith('ueventd')
def command_allowed(tokens):
if not tokens:
return True
op=tokens[0]
if op=='onrestart':
return command_allowed(tokens[1:])
if op in ('start','stop','restart'):
return len(tokens)==2 and tokens[1] in SERVICES
if op=='mount':
return len(tokens)>3 and tokens[1] in PSEUDO_FS and not tokens[2].startswith('/dev/')
if op not in COMMANDS:
return False
if any('/dev/block' in t or '/dev/mmcblk' in t or '/sys/kernel/config' in t or '/sys/class/android_usb' in t or '/config/usb_gadget' in t or '/proc/sys/kernel/panic' in t or '/proc/sysrq-trigger' in t for t in tokens[1:]):
return False
if op=='setprop' and len(tokens)>2 and tokens[1].startswith('ctl.'):
return tokens[1] in ('ctl.start','ctl.stop','ctl.restart') and tokens[2] in SERVICES
return True
def logical_lines(text):
pending = ''
first = 1
for number, line in enumerate(text.splitlines(), 1):
if not pending: first = number
pending += line
if pending.endswith('\\'):
pending = pending[:-1]+' '
continue
try:
shlex.split(pending.strip(), comments=True)
except ValueError:
pending += '\n'
continue
yield first, pending
pending = ''
if pending:
raise ValueError('Unterminated Android init command at line '+str(first))
def filter_rc(text):
output=[];removed=[];kept=[];section=None;keep=True
for number,line in logical_lines(text):
stripped=line.strip()
if not stripped or stripped.startswith('#'):
if keep:output.append(line)
continue
tokens=shlex.split(stripped,comments=True)
if not line[0].isspace():
section=tokens[0];keep=True
if section=='service':
keep=len(tokens)>2 and tokens[1] in SERVICES and not any('_DISABLED' in t for t in tokens[2:])
if keep:kept.append(tokens[1])
elif section=='import':
# USB, recovery, and Android app processes are outside this trial.
keep=not any(word in stripped for word in ('usb','recovery','zygote'))
elif section!='on':
keep=False
if keep:output.append(line)
else:removed.append({'line':number,'text':line})
continue
accepted=keep and (section=='service' or command_allowed(tokens))
# Keep failed trial services inspectable instead of rebooting the device.
if section=='service' and tokens[0]=='critical':accepted=False
if section=='service' and tokens[0]=='onrestart':accepted=keep and command_allowed(tokens)
if accepted:output.append(line)
else:removed.append({'line':number,'text':line})
return '\n'.join(output)+'\n',{'services':kept,'removed':removed}