| 1 | """Verify successful I/O inside overlapping SMB reader and writer guards.""" |
| 2 | import json |
| 3 | from collections import Counter |
| 4 | |
| 5 | |
| 6 | class PendingOverlap(AssertionError): |
| 7 | pass |
| 8 | |
| 9 | |
| 10 | def renames(request): |
| 11 | return request['command'] == 17 and request.get('info_type') == 1 and request.get('info_class') == 10 |
| 12 | |
| 13 | |
| 14 | def verify(events, phase=None): |
| 15 | pending, files, peers, pairs = {}, {}, {}, {} |
| 16 | progress = Counter() |
| 17 | path_epoch = 0 |
| 18 | active_since = -1 if phase is None else None |
| 19 | for index, event in enumerate(events): |
| 20 | assert not event.get('trace_error'), 'Trace collection failed' |
| 21 | assert not event.get('encrypted'), 'Encrypted traffic cannot establish I/O overlap' |
| 22 | if phase is not None and event.get('control', {}).get('phase') == phase: |
| 23 | active_since = index |
| 24 | connection = event.get('connection') |
| 25 | if event.get('opened'): |
| 26 | peers[connection] = event['peer'][0] |
| 27 | if event.get('closed'): |
| 28 | files = {key: value for key, value in files.items() if key[0] != connection} |
| 29 | if 'command' not in event: |
| 30 | continue |
| 31 | message = connection, event['message'] |
| 32 | if event['direction'] == 'request': |
| 33 | if renames(event): |
| 34 | files.clear() |
| 35 | path_epoch += 1 |
| 36 | pending[message] = index, event, path_epoch |
| 37 | if event['command'] == 6: |
| 38 | files.pop((connection, event['file_id']), None) |
| 39 | elif event['command'] == 10: |
| 40 | file = files.get((connection, event['file_id'])) |
| 41 | if file: |
| 42 | for offset, length, flags in event['locks']: |
| 43 | if flags & 4: |
| 44 | file['locks'] = {at: lock for at, lock in file['locks'].items() |
| 45 | if not offset <= at < offset + length} |
| 46 | continue |
| 47 | if event['status'] == '0x103': |
| 48 | continue |
| 49 | if message not in pending and event['command'] in (0, 18): |
| 50 | continue |
| 51 | request_index, request, request_epoch = pending.pop(message) |
| 52 | if renames(request): |
| 53 | files.clear() |
| 54 | path_epoch += 1 |
| 55 | if event['status'] != '0x0': |
| 56 | continue |
| 57 | command = event['command'] |
| 58 | if command == 5: |
| 59 | if request_epoch != path_epoch or any(renames(request) for _, request, _ in pending.values()): |
| 60 | continue |
| 61 | files[connection, event['file_id']] = { |
| 62 | 'path': request['path'].lower(), 'access': request['access'], |
| 63 | 'locks': {}, |
| 64 | } |
| 65 | continue |
| 66 | if command not in (8, 9, 10): |
| 67 | continue |
| 68 | key = connection, request['file_id'] |
| 69 | if key not in files: |
| 70 | continue |
| 71 | file = files[key] |
| 72 | if command == 10: |
| 73 | for offset, length, flags in request['locks']: |
| 74 | if length != 1 or offset not in (0xfffffffb, 0xfffffffd): |
| 75 | continue |
| 76 | if flags & 4: |
| 77 | file['locks'].pop(offset, None) |
| 78 | else: |
| 79 | file['locks'][offset] = index, flags & 3 |
| 80 | continue |
| 81 | if active_since is None or request_index <= active_since or not file['path'].endswith('synthetic.one') or request['length'] == 0: |
| 82 | continue |
| 83 | native = peers[connection].startswith('192.168.77.') |
| 84 | progress[('native' if native else 'host', connection, 'read' if command == 8 else 'write')] += 1 |
| 85 | for other_key, other in files.items(): |
| 86 | if key[0] == other_key[0] or file['path'] != other['path']: |
| 87 | continue |
| 88 | reader, writer = (file, other) if command == 8 else (other, file) |
| 89 | reader_key, writer_key = (key, other_key) if command == 8 else (other_key, key) |
| 90 | reader_lock = reader['locks'].get(0xfffffffb) |
| 91 | writer_lock = writer['locks'].get(0xfffffffd) |
| 92 | # Only read-only handles distinguish a reader from a writer's own validation reads. |
| 93 | if reader['access'] & 0x40000002 or not reader_lock or not writer_lock: |
| 94 | continue |
| 95 | if reader_lock[1] != 1 or writer_lock[1] != 2: |
| 96 | continue |
| 97 | if request_index <= max(reader_lock[0], writer_lock[0]): |
| 98 | continue |
| 99 | pair = reader_key + (reader_lock[0],) + writer_key + (writer_lock[0],) |
| 100 | observed = pairs.setdefault(pair, {'read': 0, 'write': 0, |
| 101 | 'reader_peer': peers[reader_key[0]], 'writer_peer': peers[writer_key[0]]}) |
| 102 | observed['read' if command == 8 else 'write'] += 1 |
| 103 | assert active_since is not None, 'Requested trace phase was not observed' |
| 104 | both = [pair for pair in pairs.values() if pair['read'] and pair['write']] |
| 105 | native_writer_pairs = [pair for pair in both if pair['writer_peer'].startswith('192.168.77.') |
| 106 | and not pair['reader_peer'].startswith('192.168.77.')] |
| 107 | rust_writer_pairs = [pair for pair in both if not pair['writer_peer'].startswith('192.168.77.')] |
| 108 | if not both: raise PendingOverlap('No reader/writer guard pair performed both successful reads and writes while overlapping') |
| 109 | if not native_writer_pairs: raise PendingOverlap('No active Rust reader overlapped successful native writes') |
| 110 | if not rust_writer_pairs: raise PendingOverlap('No active reader overlapped successful Rust writes') |
| 111 | return {'active_guard_pairs': len(both), 'active_native_writer_pairs': len(native_writer_pairs), |
| 112 | 'active_rust_writer_pairs': len(rust_writer_pairs), |
| 113 | 'overlapping_reads': sum(pair['read'] for pair in both), |
| 114 | 'overlapping_writes': sum(pair['write'] for pair in both), |
| 115 | 'connections': [{'kind': kind, 'connection': connection, 'operation': operation, 'count': count} |
| 116 | for (kind, connection, operation), count in sorted(progress.items())]} |
| 117 | |
| 118 | |
| 119 | if __name__ == '__main__': |
| 120 | import sys |
| 121 | with open(sys.argv[1]) as stream: |
| 122 | result = verify(json.loads(line) for line in stream) |
| 123 | print(json.dumps(result, indent=2)) |