Skip to content

Commit b10757c

Browse files
committed
featured: update and extend tests for deregister race guards
test_feature_resync: the sub-case where get_entry returns None previously asserted that mod_entry would be called with the rendered state. That behaviour was the root of the bug -- resync_feature_state was recreating a FEATURE row that had been deleted by a concurrent deregister. Update the assertion to assert_not_called(), matching the new guard that returns early when the entry is absent. Add a further sub-case for a partial entry (row exists but has no 'state' key, the TOCTOU artifact left by sync_feature_scope racing with deregister): resync_feature_state must call set_entry(None) to remove the ghost record rather than completing it with mod_entry. test_sync_feature_scope_toctou: new test for the post-write TOCTOU guard in sync_feature_scope. Uses side_effect to return a live entry on the pre-write check and a partial entry (no 'state' key) on the post-write check, simulating the window where the FEATURE row is deleted and recreated by mod_entry during the race. sync_feature_scope must call set_entry(None) to clean it up. Signed-off-by: david.zagury <davidza@nvidia.com>
1 parent 8c1737f commit b10757c

2 files changed

Lines changed: 43 additions & 2 deletions

File tree

scripts/featured

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ class FeatureHandler(object):
353353
new_per_asic = str(feature_config.has_per_asic_scope)
354354
new_global = str(feature_config.has_global_scope)
355355
self._conditional_update_scope(self._config_db, feature_config.name, new_per_asic, new_global)
356+
356357
# If the entry was deleted by a concurrent deregister, _conditional_update_scope
357358
# may have recreated it without a 'state' field. Remove the partial entry to
358359
# avoid a ghost record.

tests/featured/featured_test.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,8 @@ def test_feature_resync(self):
282282
}
283283
mock_db.get_entry.return_value = None
284284
feature_handler.sync_state_field(feature_table)
285-
mock_db.mod_entry.assert_called_with('FEATURE', 'sflow', {'state': 'enabled'})
285+
# Missing FEATURE row: resync_feature_state skips mod_entry (avoid recreating a row removed concurrently).
286+
mock_db.mod_entry.assert_not_called()
286287
mock_db.mod_entry.reset_mock()
287288

288289
feature_handler = featured.FeatureHandler(mock_db, mock_feature_state_table, {}, False)
@@ -321,7 +322,46 @@ def test_feature_resync(self):
321322
}
322323
feature_handler.sync_state_field(feature_table)
323324
mock_db.mod_entry.assert_called_with('FEATURE', 'sflow', {'state': 'enabled'})
324-
325+
mock_db.mod_entry.reset_mock()
326+
327+
# Partial entry (no 'state' key): left behind by sync_feature_scope racing with
328+
# deregister. resync_feature_state must delete it rather than completing it.
329+
feature_handler = featured.FeatureHandler(mock_db, mock_feature_state_table, {}, False)
330+
mock_db.get_entry.return_value = {
331+
'has_per_asic_scope': 'False',
332+
'has_global_scope': 'True',
333+
}
334+
mock_db.set_entry = mock.MagicMock()
335+
feature_handler.sync_state_field(feature_table)
336+
mock_db.set_entry.assert_called_with('FEATURE', 'sflow', None)
337+
mock_db.mod_entry.assert_not_called()
338+
339+
def test_sync_feature_scope_toctou(self):
340+
"""sync_feature_scope must delete a partial entry it accidentally creates when
341+
the FEATURE row is deleted by a concurrent deregister between the mod_entry
342+
calls and the post-write existence check (TOCTOU)."""
343+
mock_db = mock.MagicMock()
344+
mock_feature_state_table = mock.MagicMock()
345+
346+
feature_handler = featured.FeatureHandler(mock_db, mock_feature_state_table, {}, False)
347+
feature = featured.Feature('sflow', {
348+
'state': 'disabled',
349+
'has_global_scope': 'True',
350+
'has_per_asic_scope': 'False',
351+
'delayed': 'False',
352+
})
353+
354+
# Post-write check sees a partial entry (no 'state' key): the FEATURE row was
355+
# deleted by deregister and then recreated by mod_entry during the race
356+
# window, leaving only the scope fields.
357+
mock_db.get_entry.return_value = {
358+
'has_per_asic_scope': 'False',
359+
'has_global_scope': 'True',
360+
}
361+
362+
feature_handler.sync_feature_scope(feature)
363+
mock_db.set_entry.assert_called_with(featured.FEATURE_TBL, 'sflow', None)
364+
325365
def test_port_init_done_twice(self):
326366
"""There could be multiple "PortInitDone" event in case of swss
327367
restart(either due to crash or due to manual operation). swss

0 commit comments

Comments
 (0)