xref: /qemu/scripts/vmstate-static-checker.py (revision c6e1f60cc73c787317316bb2956f9a95a5daee15)
1#!/usr/bin/env python3
2#
3# Compares vmstate information stored in JSON format, obtained from
4# the -dump-vmstate QEMU command.
5#
6# Copyright 2014 Amit Shah <amit.shah@redhat.com>
7# Copyright 2014 Red Hat, Inc.
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, see <http://www.gnu.org/licenses/>.
21
22import argparse
23import json
24import sys
25
26# Count the number of errors found
27taint = 0
28
29def bump_taint():
30    global taint
31
32    # Ensure we don't wrap around or reset to 0 -- the shell only has
33    # an 8-bit return value.
34    if taint < 255:
35        taint = taint + 1
36
37
38def check_fields_match(name, s_field, d_field):
39    if s_field == d_field:
40        return True
41
42    # Some fields changed names between qemu versions.  This list
43    # is used to allow such changes in each section / description.
44    changed_names = {
45        'acpi-ghes': ['ghes_addr_le', 'hw_error_le'],
46        'apic': ['timer', 'timer_expiry'],
47        'e1000': ['dev', 'parent_obj'],
48        'ehci': ['dev', 'pcidev'],
49        'I440FX': ['dev', 'parent_obj'],
50        'ich9_ahci': ['card', 'parent_obj'],
51        'ich9-ahci': ['ahci', 'ich9_ahci'],
52        'ioh3420': ['PCIDevice', 'PCIEDevice'],
53        'ioh-3240-express-root-port': ['port.br.dev',
54                                       'parent_obj.parent_obj.parent_obj',
55                                       'port.br.dev.exp.aer_log',
56                                'parent_obj.parent_obj.parent_obj.exp.aer_log'],
57        'cirrus_vga': ['hw_cursor_x', 'vga.hw_cursor_x',
58                       'hw_cursor_y', 'vga.hw_cursor_y'],
59        'lsiscsi': ['dev', 'parent_obj'],
60        'mch': ['d', 'parent_obj'],
61        'pci_bridge': ['bridge.dev', 'parent_obj', 'bridge.dev.shpc', 'shpc'],
62        'pcnet': ['pci_dev', 'parent_obj'],
63        'PIIX3': ['pci_irq_levels', 'pci_irq_levels_vmstate'],
64        'piix4_pm': ['dev', 'parent_obj', 'pci0_status',
65                     'acpi_pci_hotplug.acpi_pcihp_pci_status[0x0]',
66                     'pm1a.sts', 'ar.pm1.evt.sts', 'pm1a.en', 'ar.pm1.evt.en',
67                     'pm1_cnt.cnt', 'ar.pm1.cnt.cnt',
68                     'tmr.timer', 'ar.tmr.timer',
69                     'tmr.overflow_time', 'ar.tmr.overflow_time',
70                     'gpe', 'ar.gpe'],
71        'rtl8139': ['dev', 'parent_obj'],
72        'qxl': ['num_surfaces', 'ssd.num_surfaces'],
73        'usb-ccid': ['abProtocolDataStructure', 'abProtocolDataStructure.data'],
74        'usb-host': ['dev', 'parent_obj'],
75        'usb-mouse': ['usb-ptr-queue', 'HIDPointerEventQueue'],
76        'usb-tablet': ['usb-ptr-queue', 'HIDPointerEventQueue'],
77        'vmware_vga': ['card', 'parent_obj'],
78        'vmware_vga_internal': ['depth', 'new_depth'],
79        'xhci': ['pci_dev', 'parent_obj'],
80        'x3130-upstream': ['PCIDevice', 'PCIEDevice'],
81        'xio3130-express-downstream-port': ['port.br.dev',
82                                            'parent_obj.parent_obj.parent_obj',
83                                            'port.br.dev.exp.aer_log',
84                                'parent_obj.parent_obj.parent_obj.exp.aer_log'],
85        'xio3130-downstream': ['PCIDevice', 'PCIEDevice'],
86        'xio3130-express-upstream-port': ['br.dev', 'parent_obj.parent_obj',
87                                          'br.dev.exp.aer_log',
88                                          'parent_obj.parent_obj.exp.aer_log'],
89        'spapr_pci': ['dma_liobn[0]', 'mig_liobn',
90                      'mem_win_addr', 'mig_mem_win_addr',
91                      'mem_win_size', 'mig_mem_win_size',
92                      'io_win_addr', 'mig_io_win_addr',
93                      'io_win_size', 'mig_io_win_size'],
94        'hpet': ['num_timers', 'num_timers_save'],
95    }
96
97    if not name in changed_names:
98        return False
99
100    if s_field in changed_names[name] and d_field in changed_names[name]:
101        return True
102
103    return False
104
105def get_changed_sec_name(sec):
106    # Section names can change -- see commit 292b1634 for an example.
107    changes = {
108        "ICH9 LPC": "ICH9-LPC",
109        "e1000-82540em": "e1000",
110    }
111
112    for item in changes:
113        if item == sec:
114            return changes[item]
115        if changes[item] == sec:
116            return item
117    return ""
118
119def exists_in_substruct(fields, item):
120    # Some QEMU versions moved a few fields inside a substruct.  This
121    # kept the on-wire format the same.  This function checks if
122    # something got shifted inside a substruct.  For example, the
123    # change in commit 1f42d22233b4f3d1a2933ff30e8d6a6d9ee2d08f
124
125    if not "Description" in fields:
126        return False
127
128    if not "Fields" in fields["Description"]:
129        return False
130
131    substruct_fields = fields["Description"]["Fields"]
132
133    if substruct_fields == []:
134        return False
135
136    return check_fields_match(fields["Description"]["name"],
137                              substruct_fields[0]["field"], item)
138
139def size_total(entry):
140    size = entry["size"]
141    if "num" not in entry:
142        return size
143    return size * entry["num"]
144
145def check_fields(src_fields, dest_fields, desc, sec):
146    # This function checks for all the fields in a section.  If some
147    # fields got embedded into a substruct, this function will also
148    # attempt to check inside the substruct.
149
150    d_iter = iter(dest_fields)
151    s_iter = iter(src_fields)
152
153    # Using these lists as stacks to store previous value of s_iter
154    # and d_iter, so that when time comes to exit out of a substruct,
155    # we can go back one level up and continue from where we left off.
156
157    s_iter_list = []
158    d_iter_list = []
159
160    advance_src = True
161    advance_dest = True
162    unused_count = 0
163
164    while True:
165        if advance_src:
166            try:
167                s_item = next(s_iter)
168            except StopIteration:
169                if s_iter_list == []:
170                    break
171
172                s_iter = s_iter_list.pop()
173                continue
174        else:
175            if unused_count == 0:
176                # We want to avoid advancing just once -- when entering a
177                # dest substruct, or when exiting one.
178                advance_src = True
179
180        if advance_dest:
181            try:
182                d_item = next(d_iter)
183            except StopIteration:
184                if d_iter_list == []:
185                    # We were not in a substruct
186                    print("Section \"" + sec + "\",", end=' ')
187                    print("Description " + "\"" + desc + "\":", end=' ')
188                    print("expected field \"" + s_item["field"] + "\",", end=' ')
189                    print("while dest has no further fields")
190                    bump_taint()
191                    break
192
193                d_iter = d_iter_list.pop()
194                advance_src = False
195                continue
196        else:
197            if unused_count == 0:
198                advance_dest = True
199
200        if unused_count != 0:
201            if advance_dest == False:
202                unused_count = unused_count - s_item["size"]
203                if unused_count == 0:
204                    advance_dest = True
205                    continue
206                if unused_count < 0:
207                    print("Section \"" + sec + "\",", end=' ')
208                    print("Description \"" + desc + "\":", end=' ')
209                    print("unused size mismatch near \"", end=' ')
210                    print(s_item["field"] + "\"")
211                    bump_taint()
212                    break
213                continue
214
215            if advance_src == False:
216                unused_count = unused_count - d_item["size"]
217                if unused_count == 0:
218                    advance_src = True
219                    continue
220                if unused_count < 0:
221                    print("Section \"" + sec + "\",", end=' ')
222                    print("Description \"" + desc + "\":", end=' ')
223                    print("unused size mismatch near \"", end=' ')
224                    print(d_item["field"] + "\"")
225                    bump_taint()
226                    break
227                continue
228
229        if not check_fields_match(desc, s_item["field"], d_item["field"]):
230            # Some fields were put in substructs, keeping the
231            # on-wire format the same, but breaking static tools
232            # like this one.
233
234            # First, check if dest has a new substruct.
235            if exists_in_substruct(d_item, s_item["field"]):
236                # listiterators don't have a prev() function, so we
237                # have to store our current location, descend into the
238                # substruct, and ensure we come out as if nothing
239                # happened when the substruct is over.
240                #
241                # Essentially we're opening the substructs that got
242                # added which didn't change the wire format.
243                d_iter_list.append(d_iter)
244                substruct_fields = d_item["Description"]["Fields"]
245                d_iter = iter(substruct_fields)
246                advance_src = False
247                continue
248
249            # Next, check if src has substruct that dest removed
250            # (can happen in backward migration: 2.0 -> 1.5)
251            if exists_in_substruct(s_item, d_item["field"]):
252                s_iter_list.append(s_iter)
253                substruct_fields = s_item["Description"]["Fields"]
254                s_iter = iter(substruct_fields)
255                advance_dest = False
256                continue
257
258            if s_item["field"] == "unused" or d_item["field"] == "unused":
259                s_size = size_total(s_item)
260                d_size = size_total(d_item)
261                if s_size == d_size:
262                    continue
263
264                if d_item["field"] == "unused":
265                    advance_dest = False
266                    unused_count = d_size - s_size;
267                    continue
268
269                if s_item["field"] == "unused":
270                    advance_src = False
271                    unused_count = s_size - d_size
272                    continue
273
274            print("Section \"" + sec + "\",", end=' ')
275            print("Description \"" + desc + "\":", end=' ')
276            print("expected field \"" + s_item["field"] + "\",", end=' ')
277            print("got \"" + d_item["field"] + "\"; skipping rest")
278            bump_taint()
279            break
280
281        check_version(s_item, d_item, sec, desc)
282
283        if not "Description" in s_item:
284            # Check size of this field only if it's not a VMSTRUCT entry
285            check_size(s_item, d_item, sec, desc, s_item["field"])
286
287        check_description_in_list(s_item, d_item, sec, desc)
288
289
290def check_subsections(src_sub, dest_sub, desc, sec):
291    for s_item in src_sub:
292        found = False
293        for d_item in dest_sub:
294            if s_item["name"] != d_item["name"]:
295                continue
296
297            found = True
298            check_descriptions(s_item, d_item, sec)
299
300        if not found:
301            print("Section \"" + sec + "\", Description \"" + desc + "\":", end=' ')
302            print("Subsection \"" + s_item["name"] + "\" not found")
303            bump_taint()
304
305
306def check_description_in_list(s_item, d_item, sec, desc):
307    if not "Description" in s_item:
308        return
309
310    if not "Description" in d_item:
311        print("Section \"" + sec + "\", Description \"" + desc + "\",", end=' ')
312        print("Field \"" + s_item["field"] + "\": missing description")
313        bump_taint()
314        return
315
316    check_descriptions(s_item["Description"], d_item["Description"], sec)
317
318
319def check_descriptions(src_desc, dest_desc, sec):
320    check_version(src_desc, dest_desc, sec, src_desc["name"])
321
322    if not check_fields_match(sec, src_desc["name"], dest_desc["name"]):
323        print("Section \"" + sec + "\":", end=' ')
324        print("Description \"" + src_desc["name"] + "\"", end=' ')
325        print("missing, got \"" + dest_desc["name"] + "\" instead; skipping")
326        bump_taint()
327        return
328
329    for f in src_desc:
330        if not f in dest_desc:
331            print("Section \"" + sec + "\"", end=' ')
332            print("Description \"" + src_desc["name"] + "\":", end=' ')
333            print("Entry \"" + f + "\" missing")
334            bump_taint()
335            continue
336
337        if f == 'Fields':
338            check_fields(src_desc[f], dest_desc[f], src_desc["name"], sec)
339
340        if f == 'Subsections':
341            check_subsections(src_desc[f], dest_desc[f], src_desc["name"], sec)
342
343
344def check_version(s, d, sec, desc=None):
345    if s["version_id"] > d["version_id"]:
346        print("Section \"" + sec + "\"", end=' ')
347        if desc:
348            print("Description \"" + desc + "\":", end=' ')
349        print("version error:", s["version_id"], ">", d["version_id"])
350        bump_taint()
351
352    if not "minimum_version_id" in d:
353        return
354
355    if s["version_id"] < d["minimum_version_id"]:
356        print("Section \"" + sec + "\"", end=' ')
357        if desc:
358            print("Description \"" + desc + "\":", end=' ')
359            print("minimum version error:", s["version_id"], "<", end=' ')
360            print(d["minimum_version_id"])
361            bump_taint()
362
363
364def check_size(s, d, sec, desc=None, field=None):
365    if s["size"] != d["size"]:
366        print("Section \"" + sec + "\"", end=' ')
367        if desc:
368            print("Description \"" + desc + "\"", end=' ')
369        if field:
370            print("Field \"" + field + "\"", end=' ')
371        print("size mismatch:", s["size"], ",", d["size"])
372        bump_taint()
373
374
375def check_machine_type(s, d):
376    if s["Name"] != d["Name"]:
377        print("Warning: checking incompatible machine types:", end=' ')
378        print("\"" + s["Name"] + "\", \"" + d["Name"] + "\"")
379
380
381def main():
382    help_text = "Parse JSON-formatted vmstate dumps from QEMU in files SRC and DEST.  Checks whether migration from SRC to DEST QEMU versions would break based on the VMSTATE information contained within the JSON outputs.  The JSON output is created from a QEMU invocation with the -dump-vmstate parameter and a filename argument to it.  Other parameters to QEMU do not matter, except the -M (machine type) parameter."
383
384    parser = argparse.ArgumentParser(description=help_text)
385    parser.add_argument('-s', '--src', type=argparse.FileType('r'),
386                        required=True,
387                        help='json dump from src qemu')
388    parser.add_argument('-d', '--dest', type=argparse.FileType('r'),
389                        required=True,
390                        help='json dump from dest qemu')
391    parser.add_argument('--reverse', required=False, default=False,
392                        action='store_true',
393                        help='reverse the direction')
394    args = parser.parse_args()
395
396    src_data = json.load(args.src)
397    dest_data = json.load(args.dest)
398    args.src.close()
399    args.dest.close()
400
401    if args.reverse:
402        temp = src_data
403        src_data = dest_data
404        dest_data = temp
405
406    for sec in src_data:
407        dest_sec = sec
408        if not dest_sec in dest_data:
409            # Either the section name got changed, or the section
410            # doesn't exist in dest.
411            dest_sec = get_changed_sec_name(sec)
412            if not dest_sec in dest_data:
413                print("Section \"" + sec + "\" does not exist in dest")
414                bump_taint()
415                continue
416
417        s = src_data[sec]
418        d = dest_data[dest_sec]
419
420        if sec == "vmschkmachine":
421            check_machine_type(s, d)
422            continue
423
424        check_version(s, d, sec)
425
426        for entry in s:
427            if not entry in d:
428                print("Section \"" + sec + "\": Entry \"" + entry + "\"", end=' ')
429                print("missing")
430                bump_taint()
431                continue
432
433            if entry == "Description":
434                check_descriptions(s[entry], d[entry], sec)
435
436    return taint
437
438
439if __name__ == '__main__':
440    sys.exit(main())
441