Page MenuHomeVyOS Platform

reproduce.py

Authored By
cr0ntab
Jun 10 2026, 12:42 AM
Size
2 KB
Referenced Files
None
Subscribers
None

reproduce.py

#!/usr/bin/env python3
#
# Minimal reproduction for the vti_updown_db.py lost-update race.
#
# This drives the exact code path the IPsec updown hook uses on an
# up-client event (src/etc/ipsec.d/vti-up-down):
#
# with open_vti_updown_db_for_create_or_update() as db:
# db.add(interface, connection, protocol)
# db.commit(supply_interface_dict)
#
# but from N processes released at the same instant by a barrier, and with a
# no-op interface_dict_supplier so it never touches a real interface (the
# named vtiN interfaces do not exist, so commit() only rewrites the state
# file and performs no `ip link` operations).
#
# strongSwan fires this hook once per CHILD_SA event, per VTI. During a
# coordinated rekey, N of them run concurrently. open_vti_updown_db_*()
# does an unlocked read-modify-write of /tmp/ipsec_vti_interfaces: every
# process reads the same copy, each writes back only its own addition, and
# the last writer's commit() wins. The rest are lost, and the hook that
# lost its add leaves that interface admin-down.
#
# Run as root on a VyOS instance:
# python3 reproduce.py
#
# Stock image: prints "LOST <N-1> of <N>". Exit code 1.
# With the patched python/vyos/utils/vti_updown_db.py in place: "0 lost". Exit 0.
import multiprocessing as mp
import os
import sys
from vyos.utils.vti_updown_db import (
open_vti_updown_db_for_create_or_update,
VTI_WANT_UP_IFLIST,
)
N = 64
def up_client(index, barrier):
"""What the up-client hook does, minus the real interface bring-up."""
barrier.wait() # release all workers into the critical section together
with open_vti_updown_db_for_create_or_update() as db:
db.add(f'vti{index}', f'peer-vti{index}', 'v4')
# no-op supplier: vtiN does not exist, so commit() only rewrites the
# state file (no `ip link`, no VTIIf instantiation).
db.commit(lambda _interface: {'disable': True})
def main():
# Pre-create an empty DB so every worker takes the existing-file (r+)
# path. This isolates the lost-update race from the separate x+ create
# race (both are fixed by the same lock).
with open(VTI_WANT_UP_IFLIST, 'w'):
pass
barrier = mp.Barrier(N)
workers = [mp.Process(target=up_client, args=(i, barrier)) for i in range(N)]
for w in workers:
w.start()
for w in workers:
w.join()
with open(VTI_WANT_UP_IFLIST) as f:
entries = [e for e in f.read().split() if e]
survived = {e.split(':')[0] for e in entries}
lost = [f'vti{i}' for i in range(N) if f'vti{i}' not in survived]
print(f'fired {N} concurrent up-client events; '
f'DB retained {len(survived)} of {N} interfaces')
if lost:
print(f'LOST {len(lost)} of {N} updates '
f'(these interfaces would be stranded admin-down): '
f'{", ".join(lost)}')
sys.exit(1)
print('0 lost: all updates serialised correctly')
sys.exit(0)
if __name__ == '__main__':
main()

File Metadata

Mime Type
text/x-script.python
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3711236
Default Alt Text
reproduce.py (2 KB)

Event Timeline