diff options
Diffstat (limited to '')
-rw-r--r-- | src/shared/discover-image.c | 499 |
1 files changed, 393 insertions, 106 deletions
diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index e8f4dfb..1079d28 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -13,6 +13,7 @@ #include <unistd.h> #include "alloc-util.h" +#include "blockdev-util.h" #include "btrfs-util.h" #include "chase.h" #include "chattr-util.h" @@ -44,9 +45,10 @@ #include "strv.h" #include "time-util.h" #include "utf8.h" +#include "vpick.h" #include "xattr-util.h" -static const char* const image_search_path[_IMAGE_CLASS_MAX] = { +const char* const image_search_path[_IMAGE_CLASS_MAX] = { [IMAGE_MACHINE] = "/etc/machines\0" /* only place symlinks here */ "/run/machines\0" /* and here too */ "/var/lib/machines\0" /* the main place for images */ @@ -74,15 +76,20 @@ static const char* const image_search_path[_IMAGE_CLASS_MAX] = { "/usr/lib/confexts\0", }; -/* Inside the initrd, use a slightly different set of search path (i.e. include .extra/sysext in extension - * search dir) */ +/* Inside the initrd, use a slightly different set of search path (i.e. include .extra/sysext/ and + * .extra/confext/ in extension search dir) */ static const char* const image_search_path_initrd[_IMAGE_CLASS_MAX] = { /* (entries that aren't listed here will get the same search path as for the non initrd-case) */ [IMAGE_SYSEXT] = "/etc/extensions\0" /* only place symlinks here */ "/run/extensions\0" /* and here too */ "/var/lib/extensions\0" /* the main place for images */ - "/.extra/sysext\0" /* put sysext picked up by systemd-stub last, since not trusted */ + "/.extra/sysext\0", /* put sysext picked up by systemd-stub last, since not trusted */ + + [IMAGE_CONFEXT] = "/run/confexts\0" /* only place symlinks here */ + "/var/lib/confexts\0" /* the main place for images */ + "/usr/local/lib/confexts\0" + "/.extra/confext\0", /* put confext picked up by systemd-stub last, since not trusted */ }; static const char* image_class_suffix_table[_IMAGE_CLASS_MAX] = { @@ -92,6 +99,15 @@ static const char* image_class_suffix_table[_IMAGE_CLASS_MAX] = { DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(image_class_suffix, ImageClass); +static const char *const image_root_table[_IMAGE_CLASS_MAX] = { + [IMAGE_MACHINE] = "/var/lib/machines", + [IMAGE_PORTABLE] = "/var/lib/portables", + [IMAGE_SYSEXT] = "/var/lib/extensions", + [IMAGE_CONFEXT] = "/var/lib/confexts", +}; + +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(image_root, ImageClass); + static Image *image_free(Image *i) { assert(i); @@ -209,43 +225,101 @@ static int image_new( return 0; } -static int extract_pretty( +static int extract_image_basename( const char *path, - const char *class_suffix, - const char *format_suffix, - char **ret) { + const char *class_suffix, /* e.g. ".sysext" (this is an optional suffix) */ + char **format_suffixes, /* e.g. ".raw" (one of these will be required) */ + char **ret_basename, + char **ret_suffix) { - _cleanup_free_ char *name = NULL; + _cleanup_free_ char *name = NULL, *suffix = NULL; int r; assert(path); - assert(ret); r = path_extract_filename(path, &name); if (r < 0) return r; - if (format_suffix) { - char *e = endswith(name, format_suffix); + if (format_suffixes) { + char *e = endswith_strv(name, format_suffixes); if (!e) /* Format suffix is required */ return -EINVAL; + if (ret_suffix) { + suffix = strdup(e); + if (!suffix) + return -ENOMEM; + } + *e = 0; } if (class_suffix) { char *e = endswith(name, class_suffix); - if (e) /* Class suffix is optional */ + if (e) { /* Class suffix is optional */ + if (ret_suffix) { + _cleanup_free_ char *j = strjoin(e, suffix); + if (!j) + return -ENOMEM; + + free_and_replace(suffix, j); + } + *e = 0; + } } if (!image_name_is_valid(name)) return -EINVAL; - *ret = TAKE_PTR(name); + if (ret_suffix) + *ret_suffix = TAKE_PTR(suffix); + + if (ret_basename) + *ret_basename = TAKE_PTR(name); + return 0; } +static int image_update_quota(Image *i, int fd) { + _cleanup_close_ int fd_close = -EBADF; + int r; + + assert(i); + + if (IMAGE_IS_VENDOR(i) || IMAGE_IS_HOST(i)) + return -EROFS; + + if (i->type != IMAGE_SUBVOLUME) + return -EOPNOTSUPP; + + if (fd < 0) { + fd_close = open(i->path, O_CLOEXEC|O_NOCTTY|O_DIRECTORY); + if (fd_close < 0) + return -errno; + fd = fd_close; + } + + r = btrfs_quota_scan_ongoing(fd); + if (r < 0) + return r; + if (r > 0) + return 0; + + BtrfsQuotaInfo quota; + r = btrfs_subvol_get_subtree_quota_fd(fd, 0, "a); + if (r < 0) + return r; + + i->usage = quota.referenced; + i->usage_exclusive = quota.exclusive; + i->limit = quota.referenced_max; + i->limit_exclusive = quota.exclusive_max; + + return 1; +} + static int image_make( ImageClass c, const char *pretty, @@ -297,7 +371,12 @@ static int image_make( return 0; if (!pretty) { - r = extract_pretty(filename, image_class_suffix_to_string(c), NULL, &pretty_buffer); + r = extract_image_basename( + filename, + image_class_suffix_to_string(c), + /* format_suffix= */ NULL, + &pretty_buffer, + /* ret_suffix= */ NULL); if (r < 0) return r; @@ -334,19 +413,7 @@ static int image_make( if (r < 0) return r; - if (btrfs_quota_scan_ongoing(fd) == 0) { - BtrfsQuotaInfo quota; - - r = btrfs_subvol_get_subtree_quota_fd(fd, 0, "a); - if (r >= 0) { - (*ret)->usage = quota.referenced; - (*ret)->usage_exclusive = quota.exclusive; - - (*ret)->limit = quota.referenced_max; - (*ret)->limit_exclusive = quota.exclusive_max; - } - } - + (void) image_update_quota(*ret, fd); return 0; } } @@ -384,7 +451,12 @@ static int image_make( (void) fd_getcrtime_at(dfd, filename, AT_SYMLINK_FOLLOW, &crtime); if (!pretty) { - r = extract_pretty(filename, image_class_suffix_to_string(c), ".raw", &pretty_buffer); + r = extract_image_basename( + filename, + image_class_suffix_to_string(c), + STRV_MAKE(".raw"), + &pretty_buffer, + /* ret_suffix= */ NULL); if (r < 0) return r; @@ -418,7 +490,12 @@ static int image_make( return 0; if (!pretty) { - r = extract_pretty(filename, NULL, NULL, &pretty_buffer); + r = extract_image_basename( + filename, + /* class_suffix= */ NULL, + /* format_suffix= */ NULL, + &pretty_buffer, + /* ret_suffix= */ NULL); if (r < 0) return r; @@ -446,8 +523,9 @@ static int image_make( read_only = true; } - if (ioctl(block_fd, BLKGETSIZE64, &size) < 0) - log_debug_errno(errno, "Failed to issue BLKGETSIZE64 on device %s/%s, ignoring: %m", path ?: strnull(parent), filename); + r = blockdev_get_device_size(block_fd, &size); + if (r < 0) + log_debug_errno(r, "Failed to issue BLKGETSIZE64 on device %s/%s, ignoring: %m", path ?: strnull(parent), filename); block_fd = safe_close(block_fd); } @@ -481,6 +559,37 @@ static const char *pick_image_search_path(ImageClass class) { return in_initrd() && image_search_path_initrd[class] ? image_search_path_initrd[class] : image_search_path[class]; } +static char **make_possible_filenames(ImageClass class, const char *image_name) { + _cleanup_strv_free_ char **l = NULL; + + assert(image_name); + + FOREACH_STRING(v_suffix, "", ".v") + FOREACH_STRING(format_suffix, "", ".raw") { + _cleanup_free_ char *j = NULL; + const char *class_suffix; + + class_suffix = image_class_suffix_to_string(class); + if (class_suffix) { + j = strjoin(image_name, class_suffix, format_suffix, v_suffix); + if (!j) + return NULL; + + if (strv_consume(&l, TAKE_PTR(j)) < 0) + return NULL; + } + + j = strjoin(image_name, format_suffix, v_suffix); + if (!j) + return NULL; + + if (strv_consume(&l, TAKE_PTR(j)) < 0) + return NULL; + } + + return TAKE_PTR(l); +} + int image_find(ImageClass class, const char *name, const char *root, @@ -496,6 +605,10 @@ int image_find(ImageClass class, if (!image_name_is_valid(name)) return -ENOENT; + _cleanup_strv_free_ char **names = make_possible_filenames(class, name); + if (!names) + return -ENOMEM; + NULSTR_FOREACH(path, pick_image_search_path(class)) { _cleanup_free_ char *resolved = NULL; _cleanup_closedir_ DIR *d = NULL; @@ -512,43 +625,97 @@ int image_find(ImageClass class, * to symlink block devices into the search path. (For now, we disable that when operating * relative to some root directory.) */ flags = root ? AT_SYMLINK_NOFOLLOW : 0; - if (fstatat(dirfd(d), name, &st, flags) < 0) { - _cleanup_free_ char *raw = NULL; - if (errno != ENOENT) - return -errno; + STRV_FOREACH(n, names) { + _cleanup_free_ char *fname_buf = NULL; + const char *fname = *n; - raw = strjoin(name, ".raw"); - if (!raw) - return -ENOMEM; + if (fstatat(dirfd(d), fname, &st, flags) < 0) { + if (errno != ENOENT) + return -errno; - if (fstatat(dirfd(d), raw, &st, flags) < 0) { - if (errno == ENOENT) + continue; /* Vanished while we were looking at it */ + } + + if (endswith(fname, ".raw")) { + if (!S_ISREG(st.st_mode)) { + log_debug("Ignoring non-regular file '%s' with .raw suffix.", fname); continue; + } - return -errno; - } + } else if (endswith(fname, ".v")) { - if (!S_ISREG(st.st_mode)) - continue; + if (!S_ISDIR(st.st_mode)) { + log_debug("Ignoring non-directory file '%s' with .v suffix.", fname); + continue; + } - r = image_make(class, name, dirfd(d), resolved, raw, &st, ret); + _cleanup_free_ char *suffix = NULL; + suffix = strdup(ASSERT_PTR(startswith(fname, name))); + if (!suffix) + return -ENOMEM; + + *ASSERT_PTR(endswith(suffix, ".v")) = 0; + + _cleanup_free_ char *vp = path_join(resolved, fname); + if (!vp) + return -ENOMEM; + + PickFilter filter = { + .type_mask = endswith(suffix, ".raw") ? (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK) : (UINT32_C(1) << DT_DIR), + .basename = name, + .architecture = _ARCHITECTURE_INVALID, + .suffix = STRV_MAKE(suffix), + }; + + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + r = path_pick(root, + /* toplevel_fd= */ AT_FDCWD, + vp, + &filter, + PICK_ARCHITECTURE|PICK_TRIES, + &result); + if (r < 0) { + log_debug_errno(r, "Failed to pick versioned image on '%s', skipping: %m", vp); + continue; + } + if (!result.path) { + log_debug("Found versioned directory '%s', without matching entry, skipping: %m", vp); + continue; + } + + /* Refresh the stat data for the discovered target */ + st = result.st; + + _cleanup_free_ char *bn = NULL; + r = path_extract_filename(result.path, &bn); + if (r < 0) { + log_debug_errno(r, "Failed to extract basename of image path '%s', skipping: %m", result.path); + continue; + } + + fname_buf = path_join(fname, bn); + if (!fname_buf) + return log_oom(); - } else { - if (!S_ISDIR(st.st_mode) && !S_ISBLK(st.st_mode)) + fname = fname_buf; + + } else if (!S_ISDIR(st.st_mode) && !S_ISBLK(st.st_mode)) { + log_debug("Ignoring non-directory and non-block device file '%s' without suffix.", fname); continue; + } - r = image_make(class, name, dirfd(d), resolved, name, &st, ret); - } - if (IN_SET(r, -ENOENT, -EMEDIUMTYPE)) - continue; - if (r < 0) - return r; + r = image_make(class, name, dirfd(d), resolved, fname, &st, ret); + if (IN_SET(r, -ENOENT, -EMEDIUMTYPE)) + continue; + if (r < 0) + return r; - if (ret) - (*ret)->discoverable = true; + if (ret) + (*ret)->discoverable = true; - return 1; + return 1; + } } if (class == IMAGE_MACHINE && streq(name, ".host")) { @@ -559,7 +726,7 @@ int image_find(ImageClass class, if (ret) (*ret)->discoverable = true; - return r; + return 1; } return -ENOENT; @@ -606,43 +773,133 @@ int image_discover( return r; FOREACH_DIRENT_ALL(de, d, return -errno) { + _cleanup_free_ char *pretty = NULL, *fname_buf = NULL; _cleanup_(image_unrefp) Image *image = NULL; - _cleanup_free_ char *pretty = NULL; + const char *fname = de->d_name; struct stat st; int flags; - if (dot_or_dot_dot(de->d_name)) + if (dot_or_dot_dot(fname)) continue; /* As mentioned above, we follow symlinks on this fstatat(), because we want to * permit people to symlink block devices into the search path. */ flags = root ? AT_SYMLINK_NOFOLLOW : 0; - if (fstatat(dirfd(d), de->d_name, &st, flags) < 0) { + if (fstatat(dirfd(d), fname, &st, flags) < 0) { if (errno == ENOENT) continue; return -errno; } - if (S_ISREG(st.st_mode)) - r = extract_pretty(de->d_name, image_class_suffix_to_string(class), ".raw", &pretty); - else if (S_ISDIR(st.st_mode)) - r = extract_pretty(de->d_name, image_class_suffix_to_string(class), NULL, &pretty); - else if (S_ISBLK(st.st_mode)) - r = extract_pretty(de->d_name, NULL, NULL, &pretty); - else { - log_debug("Skipping directory entry '%s', which is neither regular file, directory nor block device.", de->d_name); - continue; - } - if (r < 0) { - log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", de->d_name); + if (S_ISREG(st.st_mode)) { + r = extract_image_basename( + fname, + image_class_suffix_to_string(class), + STRV_MAKE(".raw"), + &pretty, + /* suffix= */ NULL); + if (r < 0) { + log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", fname); + continue; + } + } else if (S_ISDIR(st.st_mode)) { + const char *v; + + v = endswith(fname, ".v"); + if (v) { + _cleanup_free_ char *suffix = NULL, *nov = NULL; + + nov = strndup(fname, v - fname); /* Chop off the .v */ + if (!nov) + return -ENOMEM; + + r = extract_image_basename( + nov, + image_class_suffix_to_string(class), + STRV_MAKE(".raw", ""), + &pretty, + &suffix); + if (r < 0) { + log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like a versioned image.", fname); + continue; + } + + _cleanup_free_ char *vp = path_join(resolved, fname); + if (!vp) + return -ENOMEM; + + PickFilter filter = { + .type_mask = endswith(suffix, ".raw") ? (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK) : (UINT32_C(1) << DT_DIR), + .basename = pretty, + .architecture = _ARCHITECTURE_INVALID, + .suffix = STRV_MAKE(suffix), + }; + + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + r = path_pick(root, + /* toplevel_fd= */ AT_FDCWD, + vp, + &filter, + PICK_ARCHITECTURE|PICK_TRIES, + &result); + if (r < 0) { + log_debug_errno(r, "Failed to pick versioned image on '%s', skipping: %m", vp); + continue; + } + if (!result.path) { + log_debug("Found versioned directory '%s', without matching entry, skipping: %m", vp); + continue; + } + + /* Refresh the stat data for the discovered target */ + st = result.st; + + _cleanup_free_ char *bn = NULL; + r = path_extract_filename(result.path, &bn); + if (r < 0) { + log_debug_errno(r, "Failed to extract basename of image path '%s', skipping: %m", result.path); + continue; + } + + fname_buf = path_join(fname, bn); + if (!fname_buf) + return log_oom(); + + fname = fname_buf; + } else { + r = extract_image_basename( + fname, + image_class_suffix_to_string(class), + /* format_suffix= */ NULL, + &pretty, + /* ret_suffix= */ NULL); + if (r < 0) { + log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", fname); + continue; + } + } + + } else if (S_ISBLK(st.st_mode)) { + r = extract_image_basename( + fname, + /* class_suffix= */ NULL, + /* format_suffix= */ NULL, + &pretty, + /* ret_v_suffix= */ NULL); + if (r < 0) { + log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", fname); + continue; + } + } else { + log_debug("Skipping directory entry '%s', which is neither regular file, directory nor block device.", fname); continue; } if (hashmap_contains(h, pretty)) continue; - r = image_make(class, pretty, dirfd(d), resolved, de->d_name, &st, &image); + r = image_make(class, pretty, dirfd(d), resolved, fname, &st, &image); if (IN_SET(r, -ENOENT, -EMEDIUMTYPE)) continue; if (r < 0) @@ -1056,6 +1313,7 @@ int image_read_only(Image *i, bool b) { return -EOPNOTSUPP; } + i->read_only = b; return 0; } @@ -1064,7 +1322,12 @@ static void make_lock_dir(void) { (void) mkdir("/run/systemd/nspawn/locks", 0700); } -int image_path_lock(const char *path, int operation, LockFile *global, LockFile *local) { +int image_path_lock( + const char *path, + int operation, + LockFile *ret_global, + LockFile *ret_local) { + _cleanup_free_ char *p = NULL; LockFile t = LOCK_FILE_INIT; struct stat st; @@ -1072,8 +1335,7 @@ int image_path_lock(const char *path, int operation, LockFile *global, LockFile int r; assert(path); - assert(global); - assert(local); + assert(ret_local); /* Locks an image path. This actually creates two locks: one "local" one, next to the image path * itself, which might be shared via NFS. And another "global" one, in /run, that uses the @@ -1095,7 +1357,9 @@ int image_path_lock(const char *path, int operation, LockFile *global, LockFile } if (getenv_bool("SYSTEMD_NSPAWN_LOCK") == 0) { - *local = *global = (LockFile) LOCK_FILE_INIT; + *ret_local = LOCK_FILE_INIT; + if (ret_global) + *ret_global = LOCK_FILE_INIT; return 0; } @@ -1111,19 +1375,23 @@ int image_path_lock(const char *path, int operation, LockFile *global, LockFile if (exclusive) return -EBUSY; - *local = *global = (LockFile) LOCK_FILE_INIT; + *ret_local = LOCK_FILE_INIT; + if (ret_global) + *ret_global = LOCK_FILE_INIT; return 0; } - if (stat(path, &st) >= 0) { - if (S_ISBLK(st.st_mode)) - r = asprintf(&p, "/run/systemd/nspawn/locks/block-%u:%u", major(st.st_rdev), minor(st.st_rdev)); - else if (S_ISDIR(st.st_mode) || S_ISREG(st.st_mode)) - r = asprintf(&p, "/run/systemd/nspawn/locks/inode-%lu:%lu", (unsigned long) st.st_dev, (unsigned long) st.st_ino); - else - return -ENOTTY; - if (r < 0) - return -ENOMEM; + if (ret_global) { + if (stat(path, &st) >= 0) { + if (S_ISBLK(st.st_mode)) + r = asprintf(&p, "/run/systemd/nspawn/locks/block-%u:%u", major(st.st_rdev), minor(st.st_rdev)); + else if (S_ISDIR(st.st_mode) || S_ISREG(st.st_mode)) + r = asprintf(&p, "/run/systemd/nspawn/locks/inode-%lu:%lu", (unsigned long) st.st_dev, (unsigned long) st.st_ino); + else + return -ENOTTY; + if (r < 0) + return -ENOMEM; + } } /* For block devices we don't need the "local" lock, as the major/minor lock above should be @@ -1141,19 +1409,21 @@ int image_path_lock(const char *path, int operation, LockFile *global, LockFile if (p) { make_lock_dir(); - r = make_lock_file(p, operation, global); + r = make_lock_file(p, operation, ret_global); if (r < 0) { release_lock_file(&t); return r; } - } else - *global = (LockFile) LOCK_FILE_INIT; + } else if (ret_global) + *ret_global = LOCK_FILE_INIT; - *local = t; + *ret_local = t; return 0; } int image_set_limit(Image *i, uint64_t referenced_max) { + int r; + assert(i); if (IMAGE_IS_VENDOR(i) || IMAGE_IS_HOST(i)) @@ -1169,7 +1439,12 @@ int image_set_limit(Image *i, uint64_t referenced_max) { (void) btrfs_qgroup_set_limit(i->path, 0, referenced_max); (void) btrfs_subvol_auto_qgroup(i->path, 0, true); - return btrfs_subvol_set_subtree_quota_limit(i->path, 0, referenced_max); + r = btrfs_subvol_set_subtree_quota_limit(i->path, 0, referenced_max); + if (r < 0) + return r; + + (void) image_update_quota(i, -EBADF); + return 0; } int image_read_metadata(Image *i, const ImagePolicy *image_policy) { @@ -1206,7 +1481,7 @@ int image_read_metadata(Image *i, const ImagePolicy *image_policy) { else if (r >= 0) { r = read_etc_hostname(path, &hostname); if (r < 0) - log_debug_errno(errno, "Failed to read /etc/hostname of image %s: %m", i->name); + log_debug_errno(r, "Failed to read /etc/hostname of image %s: %m", i->name); } path = mfree(path); @@ -1249,8 +1524,25 @@ int image_read_metadata(Image *i, const ImagePolicy *image_policy) { case IMAGE_BLOCK: { _cleanup_(loop_device_unrefp) LoopDevice *d = NULL; _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL; - - r = loop_device_make_by_path(i->path, O_RDONLY, /* sector_size= */ UINT32_MAX, LO_FLAGS_PARTSCAN, LOCK_SH, &d); + DissectImageFlags flags = + DISSECT_IMAGE_GENERIC_ROOT | + DISSECT_IMAGE_REQUIRE_ROOT | + DISSECT_IMAGE_RELAX_VAR_CHECK | + DISSECT_IMAGE_READ_ONLY | + DISSECT_IMAGE_USR_NO_ROOT | + DISSECT_IMAGE_ADD_PARTITION_DEVICES | + DISSECT_IMAGE_PIN_PARTITION_DEVICES | + DISSECT_IMAGE_VALIDATE_OS | + DISSECT_IMAGE_VALIDATE_OS_EXT | + DISSECT_IMAGE_ALLOW_USERSPACE_VERITY; + + r = loop_device_make_by_path( + i->path, + O_RDONLY, + /* sector_size= */ UINT32_MAX, + LO_FLAGS_PARTSCAN, + LOCK_SH, + &d); if (r < 0) return r; @@ -1259,20 +1551,15 @@ int image_read_metadata(Image *i, const ImagePolicy *image_policy) { /* verity= */ NULL, /* mount_options= */ NULL, image_policy, - DISSECT_IMAGE_GENERIC_ROOT | - DISSECT_IMAGE_REQUIRE_ROOT | - DISSECT_IMAGE_RELAX_VAR_CHECK | - DISSECT_IMAGE_READ_ONLY | - DISSECT_IMAGE_USR_NO_ROOT | - DISSECT_IMAGE_ADD_PARTITION_DEVICES | - DISSECT_IMAGE_PIN_PARTITION_DEVICES, + flags, &m); if (r < 0) return r; - r = dissected_image_acquire_metadata(m, - DISSECT_IMAGE_VALIDATE_OS | - DISSECT_IMAGE_VALIDATE_OS_EXT); + r = dissected_image_acquire_metadata( + m, + /* userns_fd= */ -EBADF, + flags); if (r < 0) return r; |