From 351d190796be5be583dba1b20d8a52022a6939bb Mon Sep 17 00:00:00 2001 From: Ian Jackson Date: Thu, 12 Feb 2009 11:05:55 +0000 Subject: [PATCH] Avoid trusting client-controlled areas of xenstore. Various parts of xenstore, specifically /local/GUEST/devices, are writeable by the guest. Information from these areas must be used with care, to avoid the guest tricking qemu-dm into improperly using its privileged access to dom0 resources. To this end: * Variables and functions referring to client-controlled data have had `danger' added to their names. * There is a new sanitisation/checking arrangement for getting backend paths and information about the configuration of device backends, given the frontend information. This is so that when qemu is providing a data path which is parallel to the PV frontend/backend arrangements, it only uses the configuration from a genuine backend which is really configured to serve qemu's own guest. * For information which should be read from or written to the `vm' tree, we obtain the guest's uuid from the hypervisor (the `label' as the hypercall interface calls it) rather than reading it from the guest-controlled areas of xenstore. * The `phantom vbd' feature is disabled. It relies on guest-controlled xenstore areas indicating device paths on the guest. We do not believe this feature is currently very relevant. * We _do_ allow the guest of a stubdom qemu to mess up the correspondence between pv backends and emulated devices, in the sense that we don't mind if the guest directs qemu to use a `wrong' frontend. (cherry picked from commit 5d9302d372cf0a2951e3e0c61a5e08496de8aa2b) Conflicts: qemu-xen.h --- hw/pc.c | 2 +- hw/tpm_tis.c | 19 +-- i386-dm/helper2.c | 8 ++ qemu-xen.h | 22 ++- xen-config-host.h | 2 +- xenstore.c | 331 ++++++++++++++++++++++++++++++++-------------- 6 files changed, 268 insertions(+), 116 deletions(-) diff --git a/hw/pc.c b/hw/pc.c index c13e4537..656f755b 100644 --- a/hw/pc.c +++ b/hw/pc.c @@ -1050,7 +1050,7 @@ static void pc_init1(ram_addr_t ram_size, int vga_ram_size, } #ifdef HAS_TPM - if (has_tpm_device()) + if (has_tpm_device_danger()) tpm_tis_init(&i8259[11]); #endif diff --git a/hw/tpm_tis.c b/hw/tpm_tis.c index c02a6493..ceb0deca 100644 --- a/hw/tpm_tis.c +++ b/hw/tpm_tis.c @@ -1052,12 +1052,12 @@ static int TPM_Receive(tpmState *s, tpmBuffer *buffer) Helper functions for reading data from the xenstore such as reading virtual TPM instance information ****************************************************************************/ -int has_tpm_device(void) +int has_tpm_device_danger(void) { int ret = 0; struct xs_handle *handle = xs_daemon_open(); if (handle) { - ret = xenstore_domain_has_devtype(handle, "vtpm"); + ret = xenstore_domain_has_devtype_danger(handle, "vtpm"); xs_daemon_close(handle); } return ret; @@ -1083,10 +1083,11 @@ static uint32_t vtpm_instance_from_xenstore(void) FD_ZERO(&readfds); if (handle) { - char **e = xenstore_domain_get_devices(handle, "vtpm", &num); + char **e_danger = + xenstore_domain_get_devices_danger(handle, "vtpm", &num); int fd = xs_fileno(handle); FD_SET(fd, &readfds); - if (e) { + if (e_danger) { do { struct timeval tv = { .tv_sec = 30, @@ -1095,12 +1096,12 @@ static uint32_t vtpm_instance_from_xenstore(void) /* need to make sure that the hotplug scripts have finished */ char *status = xenstore_read_hotplug_status(handle, "vtpm", - e[0]); + e_danger[0]); if (status) { if (!strcmp(status, "connected")) { char *inst = xenstore_backend_read_variable(handle, "vtpm", - e[0], + e_danger[0], "instance"); if (1 != (sscanf(inst,"%d",&number))) number = VTPM_BAD_INSTANCE; @@ -1121,7 +1122,7 @@ static uint32_t vtpm_instance_from_xenstore(void) if (!subscribed) { rc = xenstore_subscribe_to_hotplug_status(handle, "vtpm", - e[0], + e_danger[0], token); if (rc != 0) break; @@ -1136,13 +1137,13 @@ static uint32_t vtpm_instance_from_xenstore(void) end = 1; } } while (end == 0); - free(e); + free(e_danger); } if (subscribed) { /* clean up */ xenstore_unsubscribe_from_hotplug_status(handle, "vtpm", - e[0], + e_danger[0], token); } xs_daemon_close(handle); diff --git a/i386-dm/helper2.c b/i386-dm/helper2.c index d630cb0b..dee8efd3 100644 --- a/i386-dm/helper2.c +++ b/i386-dm/helper2.c @@ -83,6 +83,14 @@ int xc_handle = -1; char domain_name[64] = "Xen-no-name"; int domid; +int domid_backend = 0; + /* 0 for now. If we ever have non-dom0 backend domains, this + * will have to be the domid of the real backend domain. + * For stubdom, this is the domain of the _real_ backend + * not of the stubdom. So still 0 unless we're in a driver + * domain case. + */ + long time_offset = 0; shared_iopage_t *shared_page = NULL; diff --git a/qemu-xen.h b/qemu-xen.h index 06d8ec9f..d0e36f92 100644 --- a/qemu-xen.h +++ b/qemu-xen.h @@ -48,14 +48,16 @@ void xenstore_write_vncport(int vnc_display); void xenstore_read_vncpasswd(int domid, char *pwbuf, size_t pwbuflen); void xenstore_write_vslots(char *vslots); -int xenstore_domain_has_devtype(struct xs_handle *handle, +int xenstore_domain_has_devtype_danger(struct xs_handle *handle, const char *devtype); -char **xenstore_domain_get_devices(struct xs_handle *handle, +char **xenstore_domain_get_devices_danger(struct xs_handle *handle, const char *devtype, unsigned int *num); char *xenstore_read_hotplug_status(struct xs_handle *handle, - const char *devtype, const char *inst); + const char *devtype, + const char *inst_danger); char *xenstore_backend_read_variable(struct xs_handle *, - const char *devtype, const char *inst, + const char *devtype, + const char *inst_danger, const char *var); int xenstore_subscribe_to_hotplug_status(struct xs_handle *handle, const char *devtype, @@ -66,6 +68,18 @@ int xenstore_unsubscribe_from_hotplug_status(struct xs_handle *handle, const char *inst, const char *token); + /* `danger' means that this parameter, variable or function refers to + * an area of xenstore which is writeable by the guest and thus must + * not be trusted by qemu code. For variables containing xenstore + * paths, `danger' can mean that both the path refers to a + * guest-writeable area (so that data read from there is dangerous + * too) and that path's value was read from somewhere controlled by + * the guest (so writing to this path is not safe either). + */ + /* When we're stubdom we don't mind doing as our domain tells us to, + * at least when it comes to running our own frontends + */ + int xenstore_vm_write(int domid, char *key, char *val); char *xenstore_vm_read(int domid, char *key, unsigned int *len); diff --git a/xen-config-host.h b/xen-config-host.h index 6e84a981..c5e019b5 100644 --- a/xen-config-host.h +++ b/xen-config-host.h @@ -1,5 +1,5 @@ extern char domain_name[64]; -extern int domid; +extern int domid, domid_backend; #include diff --git a/xenstore.c b/xenstore.c index dd487ddd..2a50584f 100644 --- a/xenstore.c +++ b/xenstore.c @@ -145,49 +145,179 @@ static int drive_name_to_index(const char *name) { return ret; } +static void xenstore_get_backend_path(char **backend, const char *devtype, + const char *frontend_dompath, + int frontend_domid, + const char *inst_danger) { + /* On entry: *backend will be passed to free() + * On succcess: *backend will be from malloc + * On failure: *backend==0 + */ + char *bpath=0; + char *frontend_path=0; + char *backend_dompath=0; + char *expected_backend=0; + char *frontend_backend_path=0; + char *backend_frontend_path=0; + char *frontend_doublecheck=0; + int len; + const char *frontend_idpath_slash; + + /* clear out return value for if we error out */ + free(*backend); + *backend = 0; + + if (strchr(inst_danger,'/')) { + fprintf(logfile, "xenstore_get_backend_path inst_danger has slash" + " which is forbidden (devtype %s)\n", devtype); + goto out; + } + + if (pasprintf(&frontend_path, "%s/device/%s/%s", + frontend_dompath, devtype, inst_danger) + == -1) goto out; + + if (pasprintf(&frontend_backend_path, "%s/backend", + frontend_path) + == -1) goto out; + + bpath = xs_read(xsh, XBT_NULL, frontend_backend_path, &len); + + /* now we must check that the backend is intended for use + * by this frontend, since the frontend's /backend xenstore node + * is writeable by the untrustworthy guest. */ + + backend_dompath = xs_get_domain_path(xsh, domid_backend); + if (!backend_dompath) goto out; + + if (pasprintf(&expected_backend, "%s/backend/%s/%lu/%s", + backend_dompath, devtype, frontend_domid, inst_danger) + == -1) goto out; + + if (strcmp(bpath, expected_backend)) { + fprintf(stderr, "frontend `%s' expected backend `%s' got `%s'," + " ignoring\n", frontend_path, expected_backend, bpath); + errno = EINVAL; + goto out; + } + + if (pasprintf(&backend_frontend_path, "%s/frontend", bpath) + == -1) goto out; + + frontend_doublecheck = xs_read(xsh, XBT_NULL, backend_frontend_path, &len); + + if (strcmp(frontend_doublecheck, frontend_path)) { + fprintf(stderr, "frontend `%s' backend `%s' points to other frontend" + " `%s', ignoring\n", frontend_path, bpath, frontend_doublecheck); + errno = EINVAL; + goto out; + } + + /* steal bpath */ + *backend = bpath; + bpath = 0; + + out: + free(bpath); + free(frontend_path); + free(backend_dompath); + free(expected_backend); + free(frontend_backend_path); + free(backend_frontend_path); + free(frontend_doublecheck); +} + +const char *xenstore_get_guest_uuid(void) { +#ifdef CONFIG_STUBDOM + return 0; +#else + + static char *already_computed; + + xc_domaininfo_t info; + int xch = -1, r, i; + char *p; + + if (already_computed) + return already_computed; + + xch = xc_interface_open(); + if (xch == -1) { + fprintf(logfile, "cannot get uuid - xc_interface_open() failed: %s\n", + strerror(errno)); + goto out; + } + r = xc_domain_getinfolist(xch, domid, 1, &info); + if (r != 1) { + fprintf(logfile, "cannot get uuid - xc_domain_getinfolist() failed:" + " %s\n", strerror(errno)); + goto out; + } + already_computed = malloc(37); + for (i = 0, p = already_computed; i < 16; i++, p += 2) { + if (i==4 || i==6 || i==8 || i==10) + *p++ = '-'; + sprintf(p, "%02x", info.handle[i]); + } + close(xch); + return already_computed; + + out: + if (xch != -1) + close(xch); + + return 0; +#endif +} + #define DIRECT_PCI_STR_LEN 160 char direct_pci_str[DIRECT_PCI_STR_LEN]; void xenstore_parse_domain_config(int hvm_domid) { - char **e = NULL; - char *buf = NULL, *path; + char **e_danger = NULL; + char *buf = NULL; char *fpath = NULL, *bpath = NULL, - *dev = NULL, *params = NULL, *type = NULL, *drv = NULL; + *dev = NULL, *params = NULL, *drv = NULL; int i, any_hdN = 0, ret; unsigned int len, num, hd_index, pci_devid = 0; BlockDriverState *bs; BlockDriver *format; + /* paths controlled by untrustworthy guest, and values read from them */ + char *danger_path; + char *danger_buf = NULL; + char *danger_type = NULL; + for(i = 0; i < MAX_DRIVES + 1; i++) media_filename[i] = NULL; + xenstore_get_guest_uuid(); + xsh = xs_daemon_open(); if (xsh == NULL) { fprintf(logfile, "Could not contact xenstore for domain config\n"); return; } - path = xs_get_domain_path(xsh, hvm_domid); - if (path == NULL) { + danger_path = xs_get_domain_path(xsh, hvm_domid); + if (danger_path == NULL) { fprintf(logfile, "xs_get_domain_path() error\n"); goto out; } - if (pasprintf(&buf, "%s/device/vbd", path) == -1) + if (pasprintf(&danger_buf, "%s/device/vbd", danger_path) == -1) goto out; - e = xs_directory(xsh, XBT_NULL, buf, &num); - if (e == NULL) + e_danger = xs_directory(xsh, XBT_NULL, danger_buf, &num); + if (e_danger == NULL) num = 0; for (i = 0; i < num; i++) { /* read the backend path */ - if (pasprintf(&buf, "%s/device/vbd/%s/backend", path, e[i]) == -1) - continue; - free(bpath); - bpath = xs_read(xsh, XBT_NULL, buf, &len); + xenstore_get_backend_path(&bpath, "vbd", danger_path, hvm_domid, + e_danger[i]); if (bpath == NULL) - continue; + continue; /* read the name of the device */ if (pasprintf(&buf, "%s/dev", bpath) == -1) continue; @@ -203,12 +333,8 @@ void xenstore_parse_domain_config(int hvm_domid) for (i = 0; i < num; i++) { format = NULL; /* don't know what the format is yet */ - /* read the backend path */ - if (pasprintf(&buf, "%s/device/vbd/%s/backend", path, e[i]) == -1) - continue; - free(bpath); - bpath = xs_read(xsh, XBT_NULL, buf, &len); + xenstore_get_backend_path(&bpath, "vbd", danger_path, hvm_domid, e_danger[i]); if (bpath == NULL) continue; /* read the name of the device */ @@ -226,10 +352,11 @@ void xenstore_parse_domain_config(int hvm_domid) if (ret) continue; /* read the type of the device */ - if (pasprintf(&buf, "%s/device/vbd/%s/device-type", path, e[i]) == -1) + if (pasprintf(&danger_buf, "%s/device/vbd/%s/device-type", + danger_path, e_danger[i]) == -1) continue; - free(type); - type = xs_read(xsh, XBT_NULL, buf, &len); + free(danger_type); + danger_type = xs_read(xsh, XBT_NULL, danger_buf, &len); if (pasprintf(&buf, "%s/params", bpath) == -1) continue; free(params); @@ -267,21 +394,29 @@ void xenstore_parse_domain_config(int hvm_domid) format = &bdrv_raw; } +#if 0 + /* Phantom VBDs are disabled because the use of paths + * from guest-controlled areas in xenstore is unsafe. + * Hopefully if they are really needed for something + * someone will shout and then we will find out what for. + */ /* * check if device has a phantom vbd; the phantom is hooked * to the frontend device (for ease of cleanup), so lookup * the frontend device, and see if there is a phantom_vbd * if there is, we will use resolution as the filename */ - if (pasprintf(&buf, "%s/device/vbd/%s/phantom_vbd", path, e[i]) == -1) + if (pasprintf(&danger_buf, "%s/device/vbd/%s/phantom_vbd", path, e_danger[i]) == -1) continue; - free(fpath); - fpath = xs_read(xsh, XBT_NULL, buf, &len); - if (fpath) { - if (pasprintf(&buf, "%s/dev", fpath) == -1) + free(danger_fpath); + danger_fpath = xs_read(xsh, XBT_NULL, danger_buf, &len); + if (danger_fpath) { + if (pasprintf(&danger_buf, "%s/dev", danger_fpath) == -1) continue; free(params); - params = xs_read(xsh, XBT_NULL, buf , &len); + params_danger = xs_read(xsh, XBT_NULL, danger_buf , &len); + DANGER DANGER params is supposedly trustworthy but here + we read it from untrusted part of xenstore if (params) { /* * wait for device, on timeout silently fail because we will @@ -290,10 +425,11 @@ void xenstore_parse_domain_config(int hvm_domid) waitForDevice(params); } } +#endif bs = bdrv_new(dev); /* check if it is a cdrom */ - if (type && !strcmp(type, "cdrom")) { + if (danger_type && !strcmp(danger_type, "cdrom")) { bdrv_set_type_hint(bs, BDRV_TYPE_CDROM); if (pasprintf(&buf, "%s/params", bpath) != -1) xs_watch(xsh, buf, dev); @@ -301,9 +437,9 @@ void xenstore_parse_domain_config(int hvm_domid) /* open device now if media present */ #ifdef CONFIG_STUBDOM - if (pasprintf(&buf, "%s/device/vbd/%s", path, e[i]) == -1) + if (pasprintf(&danger_buf, "%s/device/vbd/%s", danger_path, e_danger[i]) == -1) continue; - if (bdrv_open2(bs, buf, 0 /* snapshot */, &bdrv_vbd) == 0) { + if (bdrv_open2(bs, danger_buf, 0 /* snapshot */, &bdrv_vbd) == 0) { pstrcpy(bs->filename, sizeof(bs->filename), params); continue; } @@ -340,31 +476,31 @@ void xenstore_parse_domain_config(int hvm_domid) } #ifdef CONFIG_STUBDOM - if (pasprintf(&buf, "%s/device/vkbd", path) == -1) + if (pasprintf(&danger_buf, "%s/device/vkbd", danger_path) == -1) goto out; - free(e); - e = xs_directory(xsh, XBT_NULL, buf, &num); + free(e_danger); + e_danger = xs_directory(xsh, XBT_NULL, danger_buf, &num); - if (e) { + if (e_danger) { for (i = 0; i < num; i++) { - if (pasprintf(&buf, "%s/device/vkbd/%s", path, e[i]) == -1) + if (pasprintf(&danger_buf, "%s/device/vkbd/%s", danger_path, e_danger[i]) == -1) continue; - xenfb_connect_vkbd(buf); + xenfb_connect_vkbd(danger_buf); } } - if (pasprintf(&buf, "%s/device/vfb", path) == -1) + if (pasprintf(&danger_buf, "%s/device/vfb", danger_path) == -1) goto out; - free(e); - e = xs_directory(xsh, XBT_NULL, buf, &num); + free(e_danger); + e_danger = xs_directory(xsh, XBT_NULL, danger_buf, &num); - if (e) { + if (e_danger) { for (i = 0; i < num; i++) { - if (pasprintf(&buf, "%s/device/vfb/%s", path, e[i]) == -1) + if (pasprintf(&danger_buf, "%s/device/vfb/%s", danger_path, e_danger[i]) == -1) continue; - xenfb_connect_vfb(buf); + xenfb_connect_vfb(danger_buf); } } #endif @@ -416,13 +552,14 @@ void xenstore_parse_domain_config(int hvm_domid) } out: - free(type); + free(danger_type); free(params); free(dev); free(bpath); free(buf); - free(path); - free(e); + free(danger_buf); + free(danger_path); + free(e_danger); free(drv); return; } @@ -804,7 +941,7 @@ void xenstore_read_vncpasswd(int domid, char *pwbuf, size_t pwbuflen) /* * get all device instances of a certain type */ -char **xenstore_domain_get_devices(struct xs_handle *handle, +char **xenstore_domain_get_devices_danger(struct xs_handle *handle, const char *devtype, unsigned int *num) { char *path; @@ -829,11 +966,12 @@ char **xenstore_domain_get_devices(struct xs_handle *handle, /* * Check whether a domain has devices of the given type */ -int xenstore_domain_has_devtype(struct xs_handle *handle, const char *devtype) +int xenstore_domain_has_devtype_danger(struct xs_handle *handle, + const char *devtype) { int rc = 0; unsigned int num; - char **e = xenstore_domain_get_devices(handle, devtype, &num); + char **e = xenstore_domain_get_devices_danger(handle, devtype, &num); if (e) rc = 1; free(e); @@ -844,14 +982,22 @@ int xenstore_domain_has_devtype(struct xs_handle *handle, const char *devtype) * Function that creates a path to a variable of an instance of a * certain device */ -static char *get_device_variable_path(const char *devtype, const char *inst, +static char *get_device_variable_path(const char *devtype, + const char *inst_danger, const char *var) { char *buf = NULL; - if (pasprintf(&buf, "/local/domain/0/backend/%s/%d/%s/%s", + if (strchr(inst_danger,'/')) { + fprintf(logfile, "get_device_variable_path inst_danger has slash" + " which is forbidden (devtype %s)\n", devtype); + return NULL; + } + + if (pasprintf(&buf, "/local/domain/%d/backend/%s/%d/%s/%s", + domid_backend, devtype, domid, - inst, + inst_danger /* safe now */, var) == -1) { free(buf); buf = NULL; @@ -860,14 +1006,15 @@ static char *get_device_variable_path(const char *devtype, const char *inst, } char *xenstore_backend_read_variable(struct xs_handle *handle, - const char *devtype, const char *inst, + const char *devtype, + const char *inst_danger, const char *var) { char *value = NULL; char *buf = NULL; unsigned int len; - buf = get_device_variable_path(devtype, inst, var); + buf = get_device_variable_path(devtype, inst_danger, var); if (NULL == buf) goto out; @@ -884,9 +1031,10 @@ char *xenstore_backend_read_variable(struct xs_handle *handle, of device and its instance. */ char *xenstore_read_hotplug_status(struct xs_handle *handle, - const char *devtype, const char *inst) + const char *devtype, + const char *inst_danger) { - return xenstore_backend_read_variable(handle, devtype, inst, + return xenstore_backend_read_variable(handle, devtype, inst_danger, "hotplug-status"); } @@ -897,11 +1045,11 @@ char *xenstore_read_hotplug_status(struct xs_handle *handle, */ int xenstore_subscribe_to_hotplug_status(struct xs_handle *handle, const char *devtype, - const char *inst, + const char *inst_danger, const char *token) { int rc = 0; - char *path = get_device_variable_path(devtype, inst, "hotplug-status"); + char *path = get_device_variable_path(devtype, inst_danger, "hotplug-status"); if (path == NULL) return -1; @@ -920,12 +1068,12 @@ int xenstore_subscribe_to_hotplug_status(struct xs_handle *handle, */ int xenstore_unsubscribe_from_hotplug_status(struct xs_handle *handle, const char *devtype, - const char *inst, + const char *inst_danger, const char *token) { int rc = 0; char *path; - path = get_device_variable_path(devtype, inst, "hotplug-status"); + path = get_device_variable_path(devtype, inst_danger, "hotplug-status"); if (path == NULL) return -1; @@ -937,72 +1085,53 @@ int xenstore_unsubscribe_from_hotplug_status(struct xs_handle *handle, return rc; } -char *xenstore_vm_read(int domid, char *key, unsigned int *len) -{ - char *buf = NULL, *path = NULL, *value = NULL; - +static char *xenstore_vm_key_path(int domid, char *key) { + const char *uuid; + char *buf = NULL; + if (xsh == NULL) - goto out; + return NULL; - path = xs_get_domain_path(xsh, domid); - if (path == NULL) { - fprintf(logfile, "xs_get_domain_path(%d): error\n", domid); - goto out; - } + uuid = xenstore_get_guest_uuid(); + if (!uuid) return NULL; - pasprintf(&buf, "%s/vm", path); - free(path); - path = xs_read(xsh, XBT_NULL, buf, NULL); - if (path == NULL) { - fprintf(logfile, "xs_read(%s): read error\n", buf); - goto out; - } + if (pasprintf(&buf, "/vm/%s/%s", uuid, key) == -1) + return NULL; + return buf; +} + +char *xenstore_vm_read(int domid, char *key, unsigned int *len) +{ + char *path = NULL, *value = NULL; - pasprintf(&buf, "%s/%s", path, key); - value = xs_read(xsh, XBT_NULL, buf, len); + path = xenstore_vm_key_path(domid, key); + + value = xs_read(xsh, XBT_NULL, path, len); if (value == NULL) { - fprintf(logfile, "xs_read(%s): read error\n", buf); + fprintf(logfile, "xs_read(%s): read error\n", path); goto out; } out: free(path); - free(buf); return value; } int xenstore_vm_write(int domid, char *key, char *value) { - char *buf = NULL, *path = NULL; + char *path = NULL; int rc = -1; - if (xsh == NULL) - goto out; + path = xenstore_vm_key_path(domid, key); - path = xs_get_domain_path(xsh, domid); - if (path == NULL) { - fprintf(logfile, "xs_get_domain_path: error\n"); - goto out; - } - - pasprintf(&buf, "%s/vm", path); - free(path); - path = xs_read(xsh, XBT_NULL, buf, NULL); - if (path == NULL) { - fprintf(logfile, "xs_read(%s): read error\n", buf); - goto out; - } - - pasprintf(&buf, "%s/%s", path, key); - rc = xs_write(xsh, XBT_NULL, buf, value, strlen(value)); + rc = xs_write(xsh, XBT_NULL, path, value, strlen(value)); if (rc == 0) { - fprintf(logfile, "xs_write(%s, %s): write error\n", buf, key); + fprintf(logfile, "xs_write(%s, %s): write error\n", path, key); goto out; } out: free(path); - free(buf); return rc; } -- 2.39.5