Page MenuHomeVyOS Platform

reproduce.py

Authored By
cr0ntab
Sun, Sep 13, 3:39 AM
Size
2 KB
Referenced Files
None
Subscribers
None

reproduce.py

#!/usr/bin/env python3
#
# Minimal reproduction for concurrent entry into libvyosconfig.
#
# vyos-http-api runs configure operations in a threadpool
# (_configure_op -> run_in_threadpool(_execute_configure_op)), and that
# workflow builds Config(), which parses the running and session configs via
# ConfigTree(...) -> libvyosconfig from_string. retrieve_op is served on the
# event loop and also builds ConfigTree(res). ctypes releases the GIL for each
# foreign call, so both threads are inside the OCaml library at once, which
# corrupts the parser state and kills the process with SIGSEGV.
#
# This script drives the same library path directly: several threads build
# ConfigTree objects from the default config, compare them, and drop them (so
# ConfigTree.__del__ -> destroy runs too), while other threads parse an invalid
# config and check the error text. The work runs in a child process so a
# segfault is reported instead of killing the script.
#
# Run as root on a VyOS instance:
# python3 reproduce.py
#
# Stock image: "child process killed by SIGSEGV". Exit code 1.
# With the patched python/vyos/configtree.py in place: "0 errors". Exit code 0.
import signal
import subprocess
import sys
import threading
import time
CONFIG = '/opt/vyatta/etc/config.boot.default'
INVALID = 'interfaces {\n ethernet eth0 {\n address \n'
THREADS = 4
SECONDS = 20
def child():
from vyos.configtree import ConfigTree
config = open(CONFIG).read()
reference = ConfigTree(config)
deadline = time.time() + SECONDS
errors = []
counts = [0] * THREADS
def valid(idx):
while time.time() < deadline:
tree = ConfigTree(config)
if tree != reference:
errors.append('parsed tree differs from reference')
del tree
counts[idx] += 1
def invalid(idx):
while time.time() < deadline:
try:
ConfigTree(INVALID)
errors.append('invalid config parsed without error')
except ValueError as e:
if 'Syntax error' not in str(e):
errors.append(f'unexpected error text: {str(e)[:80]!r}')
counts[idx] += 1
threads = [
threading.Thread(target=valid if i % 2 == 0 else invalid, args=(i,))
for i in range(THREADS)
]
for t in threads:
t.start()
for t in threads:
t.join()
print(f'{sum(counts)} ConfigTree operations from {THREADS} threads in {SECONDS}s')
for e in sorted(set(errors))[:5]:
print(f' {e}')
print(f'{len(errors)} errors')
return 1 if errors else 0
def main():
if sys.argv[1:] == ['--child']:
return child()
proc = subprocess.run([sys.executable, __file__, '--child'])
if proc.returncode < 0:
name = signal.Signals(-proc.returncode).name
print(f'child process killed by {name}')
return 1
return proc.returncode
if __name__ == '__main__':
sys.exit(main())

File Metadata

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

Event Timeline