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:
@@ -2561,7 +2561,7 @@ def _ram_batch_new_state(plan):
|
|||||||
'started': time.time(), 'stopped': None, 'plan': plan,
|
'started': time.time(), 'stopped': None, 'plan': plan,
|
||||||
'session': None, 'seen': {}, 'attempts': [], 'attempts_total': 0,
|
'session': None, 'seen': {}, 'attempts': [], 'attempts_total': 0,
|
||||||
'counts': {'installed': 0, 'failed': 0, 'skipped': 0},
|
'counts': {'installed': 0, 'failed': 0, 'skipped': 0},
|
||||||
'current': None, 'notice': '', 'thread': None}
|
'current': None, 'notice': ''}
|
||||||
|
|
||||||
|
|
||||||
def _ram_batch_record(state, rec):
|
def _ram_batch_record(state, rec):
|
||||||
@@ -2699,8 +2699,10 @@ def _ram_batch_worker(state):
|
|||||||
break
|
break
|
||||||
server = _server_ref
|
server = _server_ref
|
||||||
session = getattr(server, 'card_session', 0) if server is not None else None
|
session = getattr(server, 'card_session', 0) if server is not None else None
|
||||||
connected = bool(server is not None
|
# The connection state is the module global the whole server uses
|
||||||
and getattr(server, 'connected', False))
|
# (_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:
|
with _RAM_BATCH_LOCK:
|
||||||
if state['stop']:
|
if state['stop']:
|
||||||
break
|
break
|
||||||
@@ -2726,7 +2728,6 @@ def _ram_batch_worker(state):
|
|||||||
state['current'] = None
|
state['current'] = None
|
||||||
state['phase'] = 'stopped' if state['stop'] else 'finished'
|
state['phase'] = 'stopped' if state['stop'] else 'finished'
|
||||||
state['stopped'] = time.time()
|
state['stopped'] = time.time()
|
||||||
state['thread'] = None
|
|
||||||
phase, total = state['phase'], state['attempts_total']
|
phase, total = state['phase'], state['attempts_total']
|
||||||
sys.stderr.write('RAM-BATCH: %s after %d attempts\n' % (phase, 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)
|
state = _ram_batch_new_state(plan)
|
||||||
with _RAM_BATCH_LOCK:
|
with _RAM_BATCH_LOCK:
|
||||||
_RAM_BATCH = state
|
_RAM_BATCH = state
|
||||||
thread = threading.Thread(target=_ram_batch_worker, args=(state,),
|
threading.Thread(target=_ram_batch_worker, args=(state,),
|
||||||
name='ram-batch', daemon=True)
|
name='ram-batch', daemon=True).start()
|
||||||
state['thread'] = thread
|
|
||||||
thread.start()
|
|
||||||
sys.stderr.write('RAM-BATCH: started (%d APDUs, loadfile_aid=%s)\n'
|
sys.stderr.write('RAM-BATCH: started (%d APDUs, loadfile_aid=%s)\n'
|
||||||
% (len(plan['seq']), plan['loadfile_aid']))
|
% (len(plan['seq']), plan['loadfile_aid']))
|
||||||
return state
|
return state
|
||||||
@@ -11208,6 +11207,12 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
self._send_json(resp, 400)
|
self._send_json(resp, 400)
|
||||||
self._log_resp(resp)
|
self._log_resp(resp)
|
||||||
return
|
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')
|
tar_keyset = body.get('tar_keyset')
|
||||||
if tar_keyset in (None, ''):
|
if tar_keyset in (None, ''):
|
||||||
tar_keyset = None
|
tar_keyset = None
|
||||||
@@ -11245,7 +11250,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
text,
|
text,
|
||||||
name=str(body.get('name') or ''),
|
name=str(body.get('name') or ''),
|
||||||
algorithm16=algorithm16,
|
algorithm16=algorithm16,
|
||||||
tars=body.get('tars'),
|
tars=tars,
|
||||||
tar_keyset=tar_keyset)
|
tar_keyset=tar_keyset)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
resp = {'error': str(e)}
|
resp = {'error': str(e)}
|
||||||
|
|||||||
@@ -955,6 +955,11 @@ class PresetStoreHttpTests(unittest.TestCase):
|
|||||||
status, resp = self._post('/api/presets/import-outfile', {
|
status, resp = self._post('/api/presets/import-outfile', {
|
||||||
'text': text, 'tar_keyset': 99})
|
'text': text, 'tar_keyset': 99})
|
||||||
self.assertEqual(status, 400, resp)
|
self.assertEqual(status, 400, resp)
|
||||||
|
# a non-list TAR table is the caller's error, never a silent fallback
|
||||||
|
status, resp = self._post('/api/presets/import-outfile', {
|
||||||
|
'text': text, 'tars': 'nope'})
|
||||||
|
self.assertEqual(status, 400, resp)
|
||||||
|
self.assertIn('tars', resp['error'])
|
||||||
# a misspelled algorithm or mode is refused, never silently defaulted
|
# a misspelled algorithm or mode is refused, never silently defaulted
|
||||||
status, resp = self._post('/api/presets/import-outfile', {
|
status, resp = self._post('/api/presets/import-outfile', {
|
||||||
'text': text, 'algorithm16': 'aes-256'})
|
'text': text, 'algorithm16': 'aes-256'})
|
||||||
|
|||||||
+20
-4
@@ -57,8 +57,12 @@ def _preset(iccid, name='Card', keyset=1):
|
|||||||
'tars': [tar]}
|
'tars': [tar]}
|
||||||
|
|
||||||
|
|
||||||
def _fake_server(iccid, store, connected=True, session=1):
|
def _fake_server(iccid, store, session=1):
|
||||||
return SimpleNamespace(iccid=iccid, card_presets=store, connected=connected,
|
"""A server-shaped object carrying only the attributes the real server
|
||||||
|
object has. The connection state is the module global `_CARD_CONNECTED`
|
||||||
|
(v3.28.0 review: a fake `connected` attribute here hid a bug - the worker
|
||||||
|
read it and never saw a card)."""
|
||||||
|
return SimpleNamespace(iccid=iccid, card_presets=store,
|
||||||
card_session=session, scc=object())
|
card_session=session, scc=object())
|
||||||
|
|
||||||
|
|
||||||
@@ -220,9 +224,10 @@ class BatchCardTests(unittest.TestCase):
|
|||||||
preset = _preset('8970119000004600098', 'Alfa')
|
preset = _preset('8970119000004600098', 'Alfa')
|
||||||
srv._RAM_BATCH = st
|
srv._RAM_BATCH = st
|
||||||
srv._server_ref = _fake_server('8970119000004600098', _FakeStore([preset]),
|
srv._server_ref = _fake_server('8970119000004600098', _FakeStore([preset]),
|
||||||
connected=True, session=7)
|
session=7)
|
||||||
calls = []
|
calls = []
|
||||||
with mock.patch.object(srv, '_ram_install_run',
|
with mock.patch.object(srv, '_CARD_CONNECTED', True), \
|
||||||
|
mock.patch.object(srv, '_ram_install_run',
|
||||||
lambda *a, **k: calls.append(1) or {'success': True}):
|
lambda *a, **k: calls.append(1) or {'success': True}):
|
||||||
t = threading.Thread(target=srv._ram_batch_worker, args=(st,), daemon=True)
|
t = threading.Thread(target=srv._ram_batch_worker, args=(st,), daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
@@ -262,6 +267,17 @@ class RamInstallPlanTests(unittest.TestCase):
|
|||||||
srv._ram_install_plan({'cap_hex': ''})
|
srv._ram_install_plan({'cap_hex': ''})
|
||||||
|
|
||||||
|
|
||||||
|
class BatchWorkerSourceTests(unittest.TestCase):
|
||||||
|
def test_the_worker_reads_the_module_connection_state(self):
|
||||||
|
"""The server object has no `connected` attribute - the worker must
|
||||||
|
read `_CARD_CONNECTED`, the global the rest of the server uses
|
||||||
|
(v3.28.0 review)."""
|
||||||
|
import inspect
|
||||||
|
src = inspect.getsource(srv._ram_batch_worker)
|
||||||
|
self.assertIn('_CARD_CONNECTED', src)
|
||||||
|
self.assertNotIn("'connected'", src)
|
||||||
|
|
||||||
|
|
||||||
class BatchLifecycleTests(unittest.TestCase):
|
class BatchLifecycleTests(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
srv._RAM_BATCH = None
|
srv._RAM_BATCH = None
|
||||||
|
|||||||
Reference in New Issue
Block a user