1#!/usr/bin/env python3
2# group: rw
3#
4# Test permissions taken by the mirror-top filter
5#
6# Copyright (C) 2021 Red Hat, Inc.
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20#
21
22import os
23
24from qemu import qmp
25from qemu.machine import machine
26
27import iotests
28from iotests import qemu_img
29
30
31image_size = 1 * 1024 * 1024
32source = os.path.join(iotests.test_dir, 'source.img')
33
34
35class TestMirrorTopPerms(iotests.QMPTestCase):
36    def setUp(self):
37        assert qemu_img('create', '-f', iotests.imgfmt, source,
38                        str(image_size)) == 0
39        self.vm = iotests.VM()
40        self.vm.add_drive(source)
41        self.vm.add_blockdev(f'null-co,node-name=null,size={image_size}')
42        self.vm.launch()
43
44        # Will be created by the test function itself
45        self.vm_b = None
46
47    def tearDown(self):
48        try:
49            self.vm.shutdown()
50        except machine.AbnormalShutdown:
51            pass
52
53        if self.vm_b is not None:
54            self.vm_b.shutdown()
55
56        os.remove(source)
57
58    def test_cancel(self):
59        """
60        Before commit 53431b9086b28, mirror-top used to not take any
61        permissions but WRITE and share all permissions.  Because it
62        is inserted between the source's original parents and the
63        source, there generally was no parent that would have taken or
64        unshared any permissions on the source, which means that an
65        external process could access the image unhindered by locks.
66        (Unless there was a parent above the protocol node that would
67        take its own locks, e.g. a format driver.)
68        This is bad enough, but if the mirror job is then cancelled,
69        the mirroring VM tries to take back the image, restores the
70        original permissions taken and unshared, and assumes this must
71        just work.  But it will not, and so the VM aborts.
72
73        Commit 53431b9086b28 made mirror keep the original permissions
74        and so no other process can "steal" the image.
75
76        (Note that you cannot really do the same with the target image
77        and then completing the job, because the mirror job always
78        took/unshared the correct permissions on the target.  For
79        example, it does not share READ_CONSISTENT, which makes it
80        difficult to let some other qemu process open the image.)
81        """
82
83        result = self.vm.qmp('blockdev-mirror',
84                             job_id='mirror',
85                             device='drive0',
86                             target='null',
87                             sync='full')
88        self.assert_qmp(result, 'return', {})
89
90        self.vm.event_wait('BLOCK_JOB_READY')
91
92        # We want this to fail because the image cannot be locked.
93        # If it does not fail, continue still and see what happens.
94        self.vm_b = iotests.VM(path_suffix='b')
95        # Must use -blockdev -device so we can use share-rw.
96        # (And we need share-rw=on because mirror-top was always
97        # forced to take the WRITE permission so it can write to the
98        # source image.)
99        self.vm_b.add_blockdev(f'file,node-name=drive0,filename={source}')
100        self.vm_b.add_device('virtio-blk,drive=drive0,share-rw=on')
101        try:
102            self.vm_b.launch()
103            print('ERROR: VM B launched successfully, this should not have '
104                  'happened')
105        except qmp.QMPConnectError:
106            assert 'Is another process using the image' in self.vm_b.get_log()
107
108        result = self.vm.qmp('block-job-cancel',
109                             device='mirror')
110        self.assert_qmp(result, 'return', {})
111
112        self.vm.event_wait('BLOCK_JOB_COMPLETED')
113
114
115if __name__ == '__main__':
116    # No metadata format driver supported, because they would for
117    # example always unshare the WRITE permission.  The raw driver
118    # just passes through the permissions from the guest device, and
119    # those are the permissions that we want to test.
120    iotests.main(supported_fmts=['raw'],
121                 supported_protocols=['file'])
122