• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

xapi-project / xen-api / 13457957190

21 Feb 2025 01:35PM CUT coverage: 78.516%. Remained the same
13457957190

Pull #6312

github

Vincent-lau
CA-407033: Call `receive_finalize2` synchronously

`Remote.receive_finalize2` is called at the end of SXM to clean things
up and compose the base and leaf images together. The compose operation
should only be called while the VDI is deactivated. Currently a thread
is created to call `receive_finalize2`, which could caused problems
where the VM itself gets started while the `receive_finalize2`/`VDI.compose`
is still in progress. This is not a safe operation to do.

The fix here is to simply remove the thread and make the whole operation
sequential.

Signed-off-by: Vincent Liu <shuntian.liu2@cloud.com>
Pull Request #6312: CA-407033: Call `receive_finalize2` synchronously

3512 of 4473 relevant lines covered (78.52%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

16.92
/python3/libexec/usb_reset.py
1
#!/usr/bin/env python3
2
#
3
# Copyright (C) Citrix Systems Inc.
4
#
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License as published
7
# by the Free Software Foundation; version 2.1 only. #
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU Lesser General Public License for more details.
12
#
13
# You should have received a copy of the GNU Lesser General Public License
14
# along with this program; if not, write to the Free Software Foundation, Inc.,
15
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
16
#
17
# attach
18
# ./usb_reset.py attach device -d dom-id -p pid [-r]
19
# ./usb_reset.py attach 2-2 -d 12 -p 4130
20
# ./usb_reset.py attach 2-2 -d 12 -p 4130 -r
21
# 1. reset device
22
# if without -r, do step 2~4
23
# 2. if it's the first USB device to pass-through
24
#      a) bind mount /dev /sys in chroot directory (/var/xen/qemu/root-<domid>)
25
#      b) create new cgroup devices:/qemu-<domid>,
26
#      c) blacklist all and add default device whitelist,
27
#      d) join current qemu process to this cgroup
28
# 3. save device uid/gid to /var/run/nonpersistent/usb/<device>
29
# 4. set device file uid/gid to (qemu_base + dom-id)
30
# 5. add current device to whitelist
31
#
32
# detach
33
# ./usb_reset.py detach device -d dom-id
34
# ./usb_reset.py detach 2-2 -d 12
35
# 1. restore device file uid/gid from /var/run/nonpersistent/usb/<device>
36
# 2. remove current device from whitelist
37
#
38
# cleanup
39
# ./usb_reset.py cleanup -d dom-id
40
# ./usb_reset.py cleanup -d 12
41
# 1.remove the cgroup if one has been created.
42
# 2.umount /dev, /sys from chroot directory if they are mounted.
43

44
import argparse
1✔
45
import ctypes
1✔
46
import ctypes.util
1✔
47
import errno
1✔
48
import fcntl
1✔
49
import grp
1✔
50
import xcp.logger as log  # pytype: disable=import-error
1✔
51
import logging
1✔
52
import os
1✔
53
import pwd
1✔
54
import re
1✔
55
from stat import S_ISCHR, S_ISBLK
1✔
56

57

58
def parse_arg():
1✔
59
    parser = argparse.ArgumentParser(
60
        description="script to attach, detach, cleanup for USB pass-through")
61
    subparsers = parser.add_subparsers(help="command", dest="command")
×
62

63
    attach = subparsers.add_parser("attach", help="attach a USB device")
×
64
    attach.add_argument("device", help="the target usb device")
×
65
    attach.add_argument("-d", dest="domid", type=int, required=True,
×
66
                        help="specify the domid of the VM")
67
    attach.add_argument("-p", dest="pid", type=int, required=True,
×
68
                        help="the process id of QEMU")
69
    attach.add_argument("-r", dest="reset_only", action="store_true",
×
70
                        help="reset device only, for privileged mode")
71

72
    detach = subparsers.add_parser("detach", help="detach a USB device")
×
73
    detach.add_argument("device", help="the target usb device")
×
74
    detach.add_argument("-d", dest="domid", type=int, required=True,
×
75
                        help="specify the domid of the VM")
76

77
    cleanup = subparsers.add_parser("cleanup", help="clean up chroot directory")
×
78
    cleanup.add_argument("-d", dest="domid", type=int, required=True,
×
79
                         help="specify the domid of the VM")
80

81
    return parser.parse_args()
×
82

83

84
def get_root_dir(domid):
1✔
85
    return "/var/xen/qemu/root-{}".format(domid)
×
86

87

88
def get_cg_dir(domid):
1✔
89
    return "/sys/fs/cgroup/devices/qemu-{}".format(domid)
×
90

91

92
def get_ids_path(device):
1✔
93
    usb_dir = "/var/run/nonpersistent/usb"
×
94
    try:
×
95
        os.makedirs(usb_dir)
×
96
    except OSError as e:
×
97
        if e.errno != errno.EEXIST:
×
98
            raise
×
99

100
    return os.path.join(usb_dir, device)
×
101

102

103
def save_device_ids(device):
1✔
104
    path = dev_path(device)
×
105

106
    try:
×
107
        stat = os.stat(path)
×
108
        ids_info = "{} {}".format(stat.st_uid, stat.st_gid)
×
109
    except OSError as e:
×
110
        log.error("Failed to stat {}: {}".format(path, str(e)))
×
111
        exit(1)
×
112

113
    try:
×
114
        with open(get_ids_path(device), "w") as f:
×
115
            f.write(ids_info)
×
116
    except IOError as e:
×
117
        log.error("Failed to save device ids {}: {}".format(path, str(e)))
×
118
        exit(1)
×
119

120

121
def load_device_ids(device):
1✔
122
    ids_path = get_ids_path(device)
×
123
    try:
×
124
        with open(ids_path) as f:
×
125
            uid, gid = list(map(int, f.readline().split()))
×
126
    except (IOError, ValueError) as e:
×
127
        log.error("Failed to load device ids: {}".format(str(e)))
×
128

129
    try:
×
130
        os.remove(ids_path)
×
131
    except OSError as e:
×
132
        # ignore and continue
133
        log.warning("Failed to remove device ids: {}".format(str(e)))
×
134

135
    return uid, gid  # pyright: ignore[reportPossiblyUnboundVariable] # pragma: no cover
136

137

138
# throw IOError, ValueError
139
def read_int(path):
1✔
140
    with open(path) as f:
×
141
        return int(f.readline())
×
142

143

144
def dev_path(device):
1✔
145
    # check device node pattern
146
    # example: "4-1", "1-2.1.2"
147
    pat = re.compile(r"\d+-\d+(\.\d+)*$")
×
148
    if pat.match(device) is None:
×
149
        log.error("Unexpected device node: {}".format(device))
×
150
        exit(1)
×
151
    try:
×
152
        bus = read_int("/sys/bus/usb/devices/{}/busnum".format(device))
×
153
        dev = read_int("/sys/bus/usb/devices/{}/devnum".format(device))
×
154
        return "/dev/bus/usb/{0:03d}/{1:03d}".format(bus, dev)
×
155
    except (IOError, ValueError) as e:
×
156
        log.error("Failed to get device path {}: {}".format(device, str(e)))
×
157
        exit(1)
×
158

159

160
def get_ctl(path, mode):  # type:(str, str) -> str
1✔
161
    """get the string to control device access for cgroup
162
    :param path: the device file path
163
    :param mode: either "r" or "rw"
164
    :return: the string to control device access
165
    """
166
    try:
×
167
        st = os.stat(path)
×
168
    except OSError as e:
×
169
        log.error("Failed to get stat of {}: {}".format(path, str(e)))
×
170
        raise
×
171

172
    t = ""
×
173
    if S_ISBLK(st.st_mode):
×
174
        t = "b"
×
175
    elif S_ISCHR(st.st_mode):
×
176
        t = "c"
×
177
    if t and mode in ("r", "rw"):
×
178
        return "{} {}:{} {}".format(t, os.major(st.st_rdev), os.minor(
×
179
            st.st_rdev), mode)
180
    raise RuntimeError("Failed to get control string of {}".format(path))
×
181

182

183
def _device_ctl(path, domid, allow):
1✔
184
    cg_dir = get_cg_dir(domid)
×
185
    file_name = "/devices.allow" if allow else "/devices.deny"
×
186
    try:
×
187
        with open(cg_dir + file_name, "w") as f:
×
188
            f.write(get_ctl(path, "rw"))
×
189
    except (IOError, OSError, RuntimeError) as e:
×
190
        log.error("Failed to {} {}: {}".format(
×
191
            "allow" if allow else "deny", path, str(e)))
192
        exit(1)
×
193

194

195
def allow_device(path, domid):
1✔
196
    _device_ctl(path, domid, True)
×
197

198

199
def deny_device(path, domid):
1✔
200
    _device_ctl(path, domid, False)
×
201

202

203
def setup_cgroup(domid, pid):  # type:(str, str) -> None
1✔
204
    """
205
    Associate the given process id (pid) with the given Linux kernel control group
206
    and limit it's device access to only /dev/null.
207

208
    :param domid (str): The control group ID string (passed on from the command line)
209
    :param pid (str): The process ID string (passed on from the command line)
210

211
    If the control group directory does not exist yet, the control group is created.
212

213
    - The pid goes into the file "tasks" to associate the process with the cgroup.
214
    - Deny device access by default by writing "a" to devices.deny.
215
    - Grant read-write access to /dev/null, writing it's device IDs to devices.allow.
216

217
    If any error occur during the setup process, the error is logged and
218
    the program exits with a status code of 1.
219
    """
220
    cg_dir = get_cg_dir(domid)
×
221

222
    try:
×
223
        os.mkdir(cg_dir, 0o755)
×
224
    except OSError as e:
×
225
        if e.errno != errno.EEXIST:
×
226
            log.error("Failed to create cgroup: {}".format(cg_dir))
×
227
            exit(1)
×
228

229
    try:
×
230
        # unbuffered write to ensure each one is flushed immediately
231
        # to the kernel's control group filesystem:
232
        #
233
        # The order of writes is likely not important, but the writes
234
        # may have to be a single write() system call for the entire string.
235
        #
236
        # Using the unbuffered Raw IO mode, we know the write was done
237
        # in exactly this way by the write function call itself, not later.
238
        #
239
        # With small writes like this , splitting them because of overflowing the
240
        # buffer is not expected to happen. To stay safe and keep using unbuffered I/O
241
        # We have to migrate to binary mode in python3,as python3 supports unbuffered 
242
        # raw I/O in binary mode.
243
        #
244
        with open(cg_dir + "/tasks", "wb", 0) as tasks, \
×
245
                open(cg_dir + "/devices.deny", "wb", 0) as deny, \
246
                open(cg_dir + "/devices.allow", "wb", 0) as allow:
247

248
            # deny all
249
            deny.write(b"a")
×
250

251
            # To write bytes, we've to encode the strings to bytes below:
252

253
            # grant rw access to /dev/null by default
254
            allow.write(get_ctl("/dev/null", "rw").encode())
×
255

256
            tasks.write(str(pid).encode())
×
257

258
    except (IOError, OSError, RuntimeError) as e:
×
259
        log.error("Failed to setup cgroup: {}".format(str(e)))
×
260
        exit(1)
×
261

262

263
def mount(source, target, fs, flags=0):
1✔
264
    if ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True
1✔
265
                   ).mount(source.encode(), target.encode(), fs.encode(), flags, None) < 0:
266
        log.error("Failed to mount {} ({}) to {} with flags {}: {}".
×
267
                  format(source, fs, target, flags,
268
                         os.strerror(ctypes.get_errno())))
269
        exit(1)
×
270

271

272
def umount(target):
1✔
273
    if ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True
1✔
274
                   ).umount(target.encode()) < 0:
275
        # log and continue
276
        log.error("Failed to umount {}: {}".
×
277
                  format(target, os.strerror(ctypes.get_errno())))
278

279

280
def attach(device, domid, pid, reset_only):
1✔
281
    path = dev_path(device)
×
282

283
    # reset device
284
    try:
×
285
        with open(path, "w") as f:
×
286
            # USBDEVFS_RESET _IO('U', 20)
287
            USBDEVFS_RESET = (ord('U') << 8) | 20
×
288
            fcntl.ioctl(f.fileno(), USBDEVFS_RESET, 0)
×
289
    except IOError as e:
×
290
        # log and continue
291
        log.error("Failed to reset {}: {}".format(path, str(e)))
×
292

293
    if reset_only:
×
294
        return
×
295

296
    save_device_ids(device)
×
297

298
    # set device file uid/gid
299
    try:
×
300
        os.chown(path, pwd.getpwnam("qemu_base").pw_uid + domid,
×
301
                 grp.getgrnam("qemu_base").gr_gid + domid)
302
    except OSError as e:
×
303
        log.error("Failed to chown device file {}: {}".format(path, str(e)))
×
304
        exit(1)
×
305

306
    root_dir = get_root_dir(domid)
×
307
    dev_dir = root_dir + "/dev"
×
308
    if not os.path.isdir(root_dir) or not os.path.isdir(dev_dir):
×
309
        log.error("Error: The chroot or dev directory doesn't exist")
×
310
        exit(1)
×
311

312
    if not os.path.isdir(dev_dir + "/bus"):
×
313
        # first USB device to pass-through
314
        MS_BIND = 4096  # mount flags, from fs.h
×
315
        mount("/dev", dev_dir, "", MS_BIND)
×
316
        setup_cgroup(domid, pid)
×
317

318
    sys_dir = root_dir + "/sys"
×
319
    # sys_dir could already be mounted because of PCI pass-through
320
    if not os.path.isdir(sys_dir):
×
321
        try:
×
322
            os.mkdir(sys_dir, 0o755)
×
323
        except OSError:
×
324
            log.error("Failed to create sys dir in chroot")
×
325
            exit(1)
×
326
    if not os.path.isdir(sys_dir + "/devices"):
×
327
        mount("/sys", sys_dir, "sysfs")
×
328

329
    # add device to cgroup allow list
330
    allow_device(path, domid)
×
331

332

333
def detach(device, domid):
1✔
334
    path = dev_path(device)
×
335
    uid, gid = load_device_ids(device)
×
336

337
    # restore uid, gid of the device file.
338
    try:
×
339
        os.chown(path, uid, gid)
×
340
    except OSError as e:
×
341
        log.error("Failed to chown device file {}: {}".format(path, str(e)))
×
342
        exit(1)
×
343

344
    # remove device from cgroup allow list
345
    deny_device(path, domid)
×
346

347

348
def cleanup(domid):
1✔
349
    # remove the cgroup if one has been created.
350
    if os.path.isdir(get_cg_dir(domid)):
×
351
        try:
×
352
            os.rmdir(get_cg_dir(domid))
×
353
        except OSError as e:
×
354
            # log and continue
355
            log.error("Failed to remove cgroup qemu-{}: {}"
×
356
                      .format(domid, str(e)))
357

358
    # umount /dev, /sys from chroot directory if they are mounted.
359
    root_dir = get_root_dir(domid)
×
360
    dev_dir = root_dir + "/dev"
×
361
    sys_dir = root_dir + "/sys"
×
362
    if os.path.isdir(dev_dir + "/bus"):
×
363
        umount(dev_dir)
×
364
    if os.path.isdir(sys_dir + "/devices"):
×
365
        umount(sys_dir)
×
366

367

368
if __name__ == "__main__":
1✔
369
    log.logToSyslog(level=logging.DEBUG)
×
370

371
    arg = parse_arg()
×
372

373
    if "attach" == arg.command:
×
374
        attach(arg.device, arg.domid, arg.pid, arg.reset_only)
×
375
    elif "detach" == arg.command:
×
376
        detach(arg.device, arg.domid)
×
377
    elif "cleanup" == arg.command:
×
378
        cleanup(arg.domid)
×
379
    else:
380
        log.error("Unexpected command: {}".format(arg.command))
×
381
        exit(1)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc