v3.28.0 audit: the batch worker's connection check

The v3.26.0-v3.28.0 audit found the worker reading
getattr(server, 'connected', False) - an attribute the server object has
never had.  The connection state is the module global _CARD_CONNECTED
(set by __main__ / _apply_equipped_card / _handle_card_disconnect and
read by /api/status the same way), so the check was always False and a
batch never acted on an equipped card: it would wait forever.  The unit
test hid it - the fake server invented a `connected` attribute the real
object does not have.

- The worker reads _CARD_CONNECTED.  The fake server carries only
  attributes the real object has, the worker test patches
  _CARD_CONNECTED (verified to fail on the old line), and a source pin
  keeps the worker off server.connected.
- The write-only `thread` field of the batch state is gone (nothing read
  it).
- The outfile endpoint refuses a non-list `tars` with 400 instead of
  silently falling back to the role defaults.

915 frontend + 922 Python; sw cache simple-v438 (unchanged).
This commit is contained in:
2026-10-14 15:28:12 +03:00
parent e8190c65f7
commit 80a67fb493
3 changed files with 40 additions and 14 deletions
+14 -9
View File
@@ -2561,7 +2561,7 @@ def _ram_batch_new_state(plan):
'started': time.time(), 'stopped': None, 'plan': plan,
'session': None, 'seen': {}, 'attempts': [], 'attempts_total': 0,
'counts': {'installed': 0, 'failed': 0, 'skipped': 0},
'current': None, 'notice': '', 'thread': None}
'current': None, 'notice': ''}
def _ram_batch_record(state, rec):
@@ -2699,8 +2699,10 @@ def _ram_batch_worker(state):
break
server = _server_ref
session = getattr(server, 'card_session', 0) if server is not None else None
connected = bool(server is not None
and getattr(server, 'connected', False))
# The connection state is the module global the whole server uses
# (_apply_equipped_card / _handle_card_disconnect); the server
# object itself has no `connected` attribute (v3.28.0 review).
connected = bool(server is not None and _CARD_CONNECTED)
with _RAM_BATCH_LOCK:
if state['stop']:
break
@@ -2726,7 +2728,6 @@ def _ram_batch_worker(state):
state['current'] = None
state['phase'] = 'stopped' if state['stop'] else 'finished'
state['stopped'] = time.time()
state['thread'] = None
phase, total = state['phase'], state['attempts_total']
sys.stderr.write('RAM-BATCH: %s after %d attempts\n' % (phase, total))
@@ -2745,10 +2746,8 @@ def _ram_batch_start(server, body):
state = _ram_batch_new_state(plan)
with _RAM_BATCH_LOCK:
_RAM_BATCH = state
thread = threading.Thread(target=_ram_batch_worker, args=(state,),
name='ram-batch', daemon=True)
state['thread'] = thread
thread.start()
threading.Thread(target=_ram_batch_worker, args=(state,),
name='ram-batch', daemon=True).start()
sys.stderr.write('RAM-BATCH: started (%d APDUs, loadfile_aid=%s)\n'
% (len(plan['seq']), plan['loadfile_aid']))
return state
@@ -11208,6 +11207,12 @@ class PysimHandler(BaseHTTPRequestHandler):
self._send_json(resp, 400)
self._log_resp(resp)
return
tars = body.get('tars')
if tars is not None and not isinstance(tars, list):
resp = {'error': 'tars must be a list of TAR entries'}
self._send_json(resp, 400)
self._log_resp(resp)
return
tar_keyset = body.get('tar_keyset')
if tar_keyset in (None, ''):
tar_keyset = None
@@ -11245,7 +11250,7 @@ class PysimHandler(BaseHTTPRequestHandler):
text,
name=str(body.get('name') or ''),
algorithm16=algorithm16,
tars=body.get('tars'),
tars=tars,
tar_keyset=tar_keyset)
except ValueError as e:
resp = {'error': str(e)}