xref: /qemu/scripts/vmstate-static-checker.py (revision 73f81da0a3628180409a0ae90ece19534bcdf09b)
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    }
95
96    if not name in changed_names:
97        return False
98
99    if s_field in changed_names[name] and d_field in changed_names[name]:
100        return True
101
102    return False
103
104def get_changed_sec_name(sec):
105    # Section names can change -- see commit 292b1634 for an example.
106    changes = {
107        "ICH9 LPC": "ICH9-LPC",
108        "e1000-82540em": "e1000",
109    }
110
111    for item in changes:
112        if item == sec:
113            return changes[item]
114        if changes[item] == sec:
115            return item
116    return ""
117
118def exists_in_substruct(fields, item):
119    # Some QEMU versions moved a few fields inside a substruct.  This
120    # kept the on-wire format the same.  This function checks if
121    # something got shifted inside a substruct.  For example, the
122    # change in commit 1f42d22233b4f3d1a2933ff30e8d6a6d9ee2d08f
123
124    if not "Description" in fields:
125        return False
126
127    if not "Fields" in fields["Description"]:
128        return False
129
130    substruct_fields = fields["Description"]["Fields"]
131
132    if substruct_fields == []:
133        return False
134
135    return check_fields_match(fields["Description"]["name"],
136                              substruct_fields[0]["field"], item)
137
138def size_total(entry):
139    size = entry["size"]
140    if "num" not in entry:
141        return size
142    return size * entry["num"]
143
144def check_fields(src_fields, dest_fields, desc, sec):
145    # This function checks for all the fields in a section.  If some
146    # fields got embedded into a substruct, this function will also
147    # attempt to check inside the substruct.
148
149    d_iter = iter(dest_fields)
150    s_iter = iter(src_fields)
151
152    # Using these lists as stacks to store previous value of s_iter
153    # and d_iter, so that when time comes to exit out of a substruct,
154    # we can go back one level up and continue from where we left off.
155
156    s_iter_list = []
157    d_iter_list = []
158
159    advance_src = True
160    advance_dest = True
161    unused_count = 0
162
163    while True:
164        if advance_src:
165            try:
166                s_item = next(s_iter)
167            except StopIteration:
168                if s_iter_list == []:
169                    break
170
171                s_iter = s_iter_list.pop()
172                continue
173        else:
174            if unused_count == 0:
175                # We want to avoid advancing just once -- when entering a
176                # dest substruct, or when exiting one.
177                advance_src = True
178
179        if advance_dest:
180            try:
181                d_item = next(d_iter)
182            except StopIteration:
183                if d_iter_list == []:
184                    # We were not in a substruct
185                    print("Section \"" + sec + "\",", end=' ')
186                    print("Description " + "\"" + desc + "\":", end=' ')
187                    print("expected field \"" + s_item["field"] + "\",", end=' ')
188                    print("while dest has no further fields")
189                    bump_taint()
190                    break
191
192                d_iter = d_iter_list.pop()
193                advance_src = False
194                continue
195        else:
196            if unused_count == 0:
197                advance_dest = True
198
199        if unused_count != 0:
200            if advance_dest == False:
201                unused_count = unused_count - s_item["size"]
202                if unused_count == 0:
203                    advance_dest = True
204                    continue
205                if unused_count < 0:
206                    print("Section \"" + sec + "\",", end=' ')
207                    print("Description \"" + desc + "\":", end=' ')
208                    print("unused size mismatch near \"", end=' ')
209                    print(s_item["field"] + "\"")
210                    bump_taint()
211                    break
212                continue
213
214            if advance_src == False:
215                unused_count = unused_count - d_item["size"]
216                if unused_count == 0:
217                    advance_src = True
218                    continue
219                if unused_count < 0:
220                    print("Section \"" + sec + "\",", end=' ')
221                    print("Description \"" + desc + "\":", end=' ')
222                    print("unused size mismatch near \"", end=' ')
223                    print(d_item["field"] + "\"")
224                    bump_taint()
225                    break
226                continue
227
228        if not check_fields_match(desc, s_item["field"], d_item["field"]):
229            # Some fields were put in substructs, keeping the
230            # on-wire format the same, but breaking static tools
231            # like this one.
232
233            # First, check if dest has a new substruct.
234            if exists_in_substruct(d_item, s_item["field"]):
235                # listiterators don't have a prev() function, so we
236                # have to store our current location, descend into the
237                # substruct, and ensure we come out as if nothing
238                # happened when the substruct is over.
239                #
240                # Essentially we're opening the substructs that got
241                # added which didn't change the wire format.
242                d_iter_list.append(d_iter)
243                substruct_fields = d_item["Description"]["Fields"]
244                d_iter = iter(substruct_fields)
245                advance_src = False
246                continue
247
248            # Next, check if src has substruct that dest removed
249            # (can happen in backward migration: 2.0 -> 1.5)
250            if exists_in_substruct(s_item, d_item["field"]):
251                s_iter_list.append(s_iter)
252                substruct_fields = s_item["Description"]["Fields"]
253                s_iter = iter(substruct_fields)
254                advance_dest = False
255                continue
256
257            if s_item["field"] == "unused" or d_item["field"] == "unused":
258                s_size = size_total(s_item)
259                d_size = size_total(d_item)
260                if s_size == d_size:
261                    continue
262
263                if d_item["field"] == "unused":
264                    advance_dest = False
265                    unused_count = d_size - s_size;
266                    continue
267
268                if s_item["field"] == "unused":
269                    advance_src = False
270                    unused_count = s_size - d_size
271                    continue
272
273            print("Section \"" + sec + "\",", end=' ')
274            print("Description \"" + desc + "\":", end=' ')
275            print("expected field \"" + s_item["field"] + "\",", end=' ')
276            print("got \"" + d_item["field"] + "\"; skipping rest")
277            bump_taint()
278            break
279
280        check_version(s_item, d_item, sec, desc)
281
282        if not "Description" in s_item:
283            # Check size of this field only if it's not a VMSTRUCT entry
284            check_size(s_item, d_item, sec, desc, s_item["field"])
285
286        check_description_in_list(s_item, d_item, sec, desc)
287
288
289def check_subsections(src_sub, dest_sub, desc, sec):
290    for s_item in src_sub:
291        found = False
292        for d_item in dest_sub:
293            if s_item["name"] != d_item["name"]:
294                continue
295
296            found = True
297            check_descriptions(s_item, d_item, sec)
298
299        if not found:
300            print("Section \"" + sec + "\", Description \"" + desc + "\":", end=' ')
301            print("Subsection \"" + s_item["name"] + "\" not found")
302            bump_taint()
303
304
305def check_description_in_list(s_item, d_item, sec, desc):
306    if not "Description" in s_item:
307        return
308
309    if not "Description" in d_item:
310        print("Section \"" + sec + "\", Description \"" + desc + "\",", end=' ')
311        print("Field \"" + s_item["field"] + "\": missing description")
312        bump_taint()
313        return
314
315    check_descriptions(s_item["Description"], d_item["Description"], sec)
316
317
318def check_descriptions(src_desc, dest_desc, sec):
319    check_version(src_desc, dest_desc, sec, src_desc["name"])
320
321    if not check_fields_match(sec, src_desc["name"], dest_desc["name"]):
322        print("Section \"" + sec + "\":", end=' ')
323        print("Description \"" + src_desc["name"] + "\"", end=' ')
324        print("missing, got \"" + dest_desc["name"] + "\" instead; skipping")
325        bump_taint()
326        return
327
328    for f in src_desc:
329        if not f in dest_desc:
330            print("Section \"" + sec + "\"", end=' ')
331            print("Description \"" + src_desc["name"] + "\":", end=' ')
332            print("Entry \"" + f + "\" missing")
333            bump_taint()
334            continue
335
336        if f == 'Fields':
337            check_fields(src_desc[f], dest_desc[f], src_desc["name"], sec)
338
339        if f == 'Subsections':
340            check_subsections(src_desc[f], dest_desc[f], src_desc["name"], sec)
341
342
343def check_version(s, d, sec, desc=None):
344    if s["version_id"] > d["version_id"]:
345        print("Section \"" + sec + "\"", end=' ')
346        if desc:
347            print("Description \"" + desc + "\":", end=' ')
348        print("version error:", s["version_id"], ">", d["version_id"])
349        bump_taint()
350
351    if not "minimum_version_id" in d:
352        return
353
354    if s["version_id"] < d["minimum_version_id"]:
355        print("Section \"" + sec + "\"", end=' ')
356        if desc:
357            print("Description \"" + desc + "\":", end=' ')
358            print("minimum version error:", s["version_id"], "<", end=' ')
359            print(d["minimum_version_id"])
360            bump_taint()
361
362
363def check_size(s, d, sec, desc=None, field=None):
364    if s["size"] != d["size"]:
365        print("Section \"" + sec + "\"", end=' ')
366        if desc:
367            print("Description \"" + desc + "\"", end=' ')
368        if field:
369            print("Field \"" + field + "\"", end=' ')
370        print("size mismatch:", s["size"], ",", d["size"])
371        bump_taint()
372
373
374def check_machine_type(s, d):
375    if s["Name"] != d["Name"]:
376        print("Warning: checking incompatible machine types:", end=' ')
377        print("\"" + s["Name"] + "\", \"" + d["Name"] + "\"")
378
379
380def main():
381    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."
382
383    parser = argparse.ArgumentParser(description=help_text)
384    parser.add_argument('-s', '--src', type=argparse.FileType('r'),
385                        required=True,
386                        help='json dump from src qemu')
387    parser.add_argument('-d', '--dest', type=argparse.FileType('r'),
388                        required=True,
389                        help='json dump from dest qemu')
390    parser.add_argument('--reverse', required=False, default=False,
391                        action='store_true',
392                        help='reverse the direction')
393    args = parser.parse_args()
394
395    src_data = json.load(args.src)
396    dest_data = json.load(args.dest)
397    args.src.close()
398    args.dest.close()
399
400    if args.reverse:
401        temp = src_data
402        src_data = dest_data
403        dest_data = temp
404
405    for sec in src_data:
406        dest_sec = sec
407        if not dest_sec in dest_data:
408            # Either the section name got changed, or the section
409            # doesn't exist in dest.
410            dest_sec = get_changed_sec_name(sec)
411            if not dest_sec in dest_data:
412                print("Section \"" + sec + "\" does not exist in dest")
413                bump_taint()
414                continue
415
416        s = src_data[sec]
417        d = dest_data[dest_sec]
418
419        if sec == "vmschkmachine":
420            check_machine_type(s, d)
421            continue
422
423        check_version(s, d, sec)
424
425        for entry in s:
426            if not entry in d:
427                print("Section \"" + sec + "\": Entry \"" + entry + "\"", end=' ')
428                print("missing")
429                bump_taint()
430                continue
431
432            if entry == "Description":
433                check_descriptions(s[entry], d[entry], sec)
434
435    return taint
436
437
438if __name__ == '__main__':
439    sys.exit(main())
440