From e749c579acbf4c5ddef4c875635af6da89e17d17 Mon Sep 17 00:00:00 2001 From: George McCollister Date: Mon, 25 Feb 2019 10:37:12 -0600 Subject: systemd: fix CVE-2018-6954 Apply patches to fix CVE-2018-6954 NVD description from https://nvd.nist.gov/vuln/detail/CVE-2018-6954 systemd-tmpfiles in systemd through 237 mishandles symlinks present in non-terminal path components, which allows local users to obtain ownership of arbitrary files via vectors involving creation of a directory and a file under that directory, and later replacing that directory with a symlink. This occurs even if the fs.protected_symlinks sysctl is turned on. Patches from systemd_237-3ubuntu10.13.debian. These patches shouldn't be required on newer OE releases since they use systemd v239 or higher. Signed-off-by: George McCollister --- ...n-t-resolve-pathnames-when-traversing-rec.patch | 643 +++++++++++++++++++++ 1 file changed, 643 insertions(+) create mode 100644 meta/recipes-core/systemd/systemd/0001-tmpfiles-don-t-resolve-pathnames-when-traversing-rec.patch (limited to 'meta/recipes-core/systemd/systemd/0001-tmpfiles-don-t-resolve-pathnames-when-traversing-rec.patch') diff --git a/meta/recipes-core/systemd/systemd/0001-tmpfiles-don-t-resolve-pathnames-when-traversing-rec.patch b/meta/recipes-core/systemd/systemd/0001-tmpfiles-don-t-resolve-pathnames-when-traversing-rec.patch new file mode 100644 index 0000000000..108e4ad8b8 --- /dev/null +++ b/meta/recipes-core/systemd/systemd/0001-tmpfiles-don-t-resolve-pathnames-when-traversing-rec.patch @@ -0,0 +1,643 @@ +From 33dc9a280f952f503e5493ee29f6815bef29d551 Mon Sep 17 00:00:00 2001 +From: Franck Bui +Date: Fri, 2 Mar 2018 17:19:32 +0100 +Subject: [PATCH] tmpfiles: don't resolve pathnames when traversing recursively + through directory trees + +Otherwise we can be fooled if one path component is replaced underneath us. + +The patch achieves that by always operating at file descriptor level (by using +*at() helpers) and by making sure we do not any path resolution when traversing +direcotry trees. + +However this is not always possible, for instance when listing the content of a +directory or some operations don't provide the *at() helpers or others (such as +fchmodat()) don't have the AT_EMPTY_PATH flag. In such cases we operate on +/proc/self/fd/%i pseudo-symlink instead, which works the same for all kinds of +objects and requires no checking of type beforehand. + +Also O_PATH flag is used when opening file objects in order to prevent +undesired behaviors: device nodes from reacting, automounts from +triggering, etc... + +Fixes: CVE-2018-6954 + +Origin: upstream, https://github.com/systemd/systemd/commit/936f6bdb803c432578e2cdcc5f93f3bfff93aff0 +Bug: https://github.com/systemd/systemd/issues/7986 + +Patch from: +systemd_237-3ubuntu10.13.debian CVE-2018-6954.patch + +https://usn.ubuntu.com/3816-1/ states that CVE-2018-6954 doesn't +affect Ubuntu 18.10 which uses the same version of systemd as thud +(239). + +CVE: CVE-2018-6954 +Upstream-Status: Backport + +Signed-off-by: George McCollister +--- + src/tmpfiles/tmpfiles.c | 363 +++++++++++++++++++++++++++++++----------------- + 1 file changed, 239 insertions(+), 124 deletions(-) + +diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c +index 88cc543f09..613d418eb3 100644 +--- a/src/tmpfiles/tmpfiles.c ++++ b/src/tmpfiles/tmpfiles.c +@@ -792,94 +792,105 @@ static bool hardlink_vulnerable(struct stat *st) { + return !S_ISDIR(st->st_mode) && st->st_nlink > 1 && dangerous_hardlinks(); + } + +-static int path_set_perms(Item *i, const char *path) { +- char fn[STRLEN("/proc/self/fd/") + DECIMAL_STR_MAX(int)]; +- _cleanup_close_ int fd = -1; +- struct stat st; ++static int fd_set_perms(Item *i, int fd, const struct stat *st) { ++ _cleanup_free_ char *path = NULL; ++ int r; + + assert(i); +- assert(path); +- +- if (!i->mode_set && !i->uid_set && !i->gid_set) +- goto shortcut; +- +- /* We open the file with O_PATH here, to make the operation +- * somewhat atomic. Also there's unfortunately no fchmodat() +- * with AT_SYMLINK_NOFOLLOW, hence we emulate it here via +- * O_PATH. */ +- +- fd = open(path, O_NOFOLLOW|O_CLOEXEC|O_PATH); +- if (fd < 0) { +- int level = LOG_ERR, r = -errno; ++ assert(fd); + +- /* Option "e" operates only on existing objects. Do not +- * print errors about non-existent files or directories */ +- if (i->type == EMPTY_DIRECTORY && errno == ENOENT) { +- level = LOG_DEBUG; +- r = 0; +- } +- +- log_full_errno(level, errno, "Adjusting owner and mode for %s failed: %m", path); ++ r = fd_get_path(fd, &path); ++ if (r < 0) + return r; +- } + +- if (fstatat(fd, "", &st, AT_EMPTY_PATH) < 0) +- return log_error_errno(errno, "Failed to fstat() file %s: %m", path); ++ if (!i->mode_set && !i->uid_set && !i->gid_set) ++ goto shortcut; + +- if (hardlink_vulnerable(&st)) { ++ if (hardlink_vulnerable(st)) { + log_error("Refusing to set permissions on hardlinked file %s while the fs.protected_hardlinks sysctl is turned off.", path); + return -EPERM; + } + +- xsprintf(fn, "/proc/self/fd/%i", fd); +- + if (i->mode_set) { +- if (S_ISLNK(st.st_mode)) ++ if (S_ISLNK(st->st_mode)) + log_debug("Skipping mode fix for symlink %s.", path); + else { + mode_t m = i->mode; + + if (i->mask_perms) { +- if (!(st.st_mode & 0111)) ++ if (!(st->st_mode & 0111)) + m &= ~0111; +- if (!(st.st_mode & 0222)) ++ if (!(st->st_mode & 0222)) + m &= ~0222; +- if (!(st.st_mode & 0444)) ++ if (!(st->st_mode & 0444)) + m &= ~0444; +- if (!S_ISDIR(st.st_mode)) ++ if (!S_ISDIR(st->st_mode)) + m &= ~07000; /* remove sticky/sgid/suid bit, unless directory */ + } + +- if (m == (st.st_mode & 07777)) +- log_debug("\"%s\" has correct mode %o already.", path, st.st_mode); ++ if (m == (st->st_mode & 07777)) ++ log_debug("\"%s\" has correct mode %o already.", path, st->st_mode); + else { ++ char procfs_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(int)]; ++ + log_debug("Changing \"%s\" to mode %o.", path, m); + +- if (chmod(fn, m) < 0) +- return log_error_errno(errno, "chmod() of %s via %s failed: %m", path, fn); ++ /* fchmodat() still doesn't have AT_EMPTY_PATH flag. */ ++ xsprintf(procfs_path, "/proc/self/fd/%i", fd); ++ ++ if (chmod(procfs_path, m) < 0) ++ return log_error_errno(errno, "chmod() of %s via %s failed: %m", path, procfs_path); + } + } + } + +- if ((i->uid_set && i->uid != st.st_uid) || +- (i->gid_set && i->gid != st.st_gid)) { ++ if ((i->uid_set && i->uid != st->st_uid) || ++ (i->gid_set && i->gid != st->st_gid)) { + log_debug("Changing \"%s\" to owner "UID_FMT":"GID_FMT, + path, + i->uid_set ? i->uid : UID_INVALID, + i->gid_set ? i->gid : GID_INVALID); + +- if (chown(fn, +- i->uid_set ? i->uid : UID_INVALID, +- i->gid_set ? i->gid : GID_INVALID) < 0) +- return log_error_errno(errno, "chown() of %s via %s failed: %m", path, fn); ++ if (fchownat(fd, ++ "", ++ i->uid_set ? i->uid : UID_INVALID, ++ i->gid_set ? i->gid : GID_INVALID, ++ AT_EMPTY_PATH) < 0) ++ return log_error_errno(errno, "fchownat() of %s failed: %m", path); + } + +- fd = safe_close(fd); +- + shortcut: + return label_fix(path, false, false); + } + ++static int path_set_perms(Item *i, const char *path) { ++ _cleanup_close_ int fd = -1; ++ struct stat st; ++ ++ assert(i); ++ assert(path); ++ ++ fd = open(path, O_NOFOLLOW|O_CLOEXEC|O_PATH); ++ if (fd < 0) { ++ int level = LOG_ERR, r = -errno; ++ ++ /* Option "e" operates only on existing objects. Do not ++ * print errors about non-existent files or directories */ ++ if (i->type == EMPTY_DIRECTORY && errno == ENOENT) { ++ level = LOG_DEBUG; ++ r = 0; ++ } ++ ++ log_full_errno(level, errno, "Adjusting owner and mode for %s failed: %m", path); ++ return r; ++ } ++ ++ if (fstat(fd, &st) < 0) ++ return log_error_errno(errno, "Failed to fstat() file %s: %m", path); ++ ++ return fd_set_perms(i, fd, &st); ++} ++ + static int parse_xattrs_from_arg(Item *i) { + const char *p; + int r; +@@ -918,21 +929,43 @@ static int parse_xattrs_from_arg(Item *i) { + return 0; + } + +-static int path_set_xattrs(Item *i, const char *path) { ++static int fd_set_xattrs(Item *i, int fd, const struct stat *st) { ++ char procfs_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(int)]; ++ _cleanup_free_ char *path = NULL; + char **name, **value; ++ int r; + + assert(i); +- assert(path); ++ assert(fd); ++ ++ r = fd_get_path(fd, &path); ++ if (r < 0) ++ return r; ++ ++ xsprintf(procfs_path, "/proc/self/fd/%i", fd); + + STRV_FOREACH_PAIR(name, value, i->xattrs) { + log_debug("Setting extended attribute '%s=%s' on %s.", *name, *value, path); +- if (lsetxattr(path, *name, *value, strlen(*value), 0) < 0) ++ if (setxattr(procfs_path, *name, *value, strlen(*value), 0) < 0) + return log_error_errno(errno, "Setting extended attribute %s=%s on %s failed: %m", + *name, *value, path); + } + return 0; + } + ++static int path_set_xattrs(Item *i, const char *path) { ++ _cleanup_close_ int fd = -1; ++ ++ assert(i); ++ assert(path); ++ ++ fd = open(path, O_CLOEXEC|O_NOFOLLOW|O_PATH); ++ if (fd < 0) ++ return log_error_errno(errno, "Cannot open '%s': %m", path); ++ ++ return fd_set_xattrs(i, fd, NULL); ++} ++ + static int parse_acls_from_arg(Item *item) { + #if HAVE_ACL + int r; +@@ -998,52 +1031,71 @@ static int path_set_acl(const char *path, const char *pretty, acl_type_t type, a + } + #endif + +-static int path_set_acls(Item *item, const char *path) { ++static int fd_set_acls(Item *item, int fd, const struct stat *st) { + int r = 0; + #if HAVE_ACL +- char fn[STRLEN("/proc/self/fd/") + DECIMAL_STR_MAX(int)]; +- _cleanup_close_ int fd = -1; +- struct stat st; ++ char procfs_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(int)]; ++ _cleanup_free_ char *path = NULL; + + assert(item); +- assert(path); +- +- fd = open(path, O_NOFOLLOW|O_CLOEXEC|O_PATH); +- if (fd < 0) +- return log_error_errno(errno, "Adjusting ACL of %s failed: %m", path); ++ assert(fd); ++ assert(st); + +- if (fstatat(fd, "", &st, AT_EMPTY_PATH) < 0) +- return log_error_errno(errno, "Failed to fstat() file %s: %m", path); ++ r = fd_get_path(fd, &path); ++ if (r < 0) ++ return r; + +- if (hardlink_vulnerable(&st)) { ++ if (hardlink_vulnerable(st)) { + log_error("Refusing to set ACLs on hardlinked file %s while the fs.protected_hardlinks sysctl is turned off.", path); + return -EPERM; + } + +- if (S_ISLNK(st.st_mode)) { ++ if (S_ISLNK(st->st_mode)) { + log_debug("Skipping ACL fix for symlink %s.", path); + return 0; + } + +- xsprintf(fn, "/proc/self/fd/%i", fd); ++ xsprintf(procfs_path, "/proc/self/fd/%i", fd); + + if (item->acl_access) +- r = path_set_acl(fn, path, ACL_TYPE_ACCESS, item->acl_access, item->force); ++ r = path_set_acl(procfs_path, path, ACL_TYPE_ACCESS, item->acl_access, item->force); + + if (r == 0 && item->acl_default) +- r = path_set_acl(fn, path, ACL_TYPE_DEFAULT, item->acl_default, item->force); ++ r = path_set_acl(procfs_path, path, ACL_TYPE_DEFAULT, item->acl_default, item->force); + + if (r > 0) + return -r; /* already warned */ +- else if (r == -EOPNOTSUPP) { ++ if (r == -EOPNOTSUPP) { + log_debug_errno(r, "ACLs not supported by file system at %s", path); + return 0; +- } else if (r < 0) +- log_error_errno(r, "ACL operation on \"%s\" failed: %m", path); ++ } ++ if (r < 0) ++ return log_error_errno(r, "ACL operation on \"%s\" failed: %m", path); + #endif + return r; + } + ++static int path_set_acls(Item *item, const char *path) { ++ int r = 0; ++#ifdef HAVE_ACL ++ _cleanup_close_ int fd = -1; ++ struct stat st; ++ ++ assert(item); ++ assert(path); ++ ++ fd = open(path, O_NOFOLLOW|O_CLOEXEC|O_PATH); ++ if (fd < 0) ++ return log_error_errno(errno, "Adjusting ACL of %s failed: %m", path); ++ ++ if (fstat(fd, &st) < 0) ++ return log_error_errno(errno, "Failed to fstat() file %s: %m", path); ++ ++ r = fd_set_acls(item, fd, &st); ++ #endif ++ return r; ++ } ++ + #define ATTRIBUTES_ALL \ + (FS_NOATIME_FL | \ + FS_SYNC_FL | \ +@@ -1143,30 +1195,24 @@ static int parse_attribute_from_arg(Item *item) { + return 0; + } + +-static int path_set_attribute(Item *item, const char *path) { +- _cleanup_close_ int fd = -1; +- struct stat st; ++static int fd_set_attribute(Item *item, int fd, const struct stat *st) { ++ char procfs_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(int)]; ++ _cleanup_close_ int procfs_fd = -1; ++ _cleanup_free_ char *path = NULL; + unsigned f; + int r; + + if (!item->attribute_set || item->attribute_mask == 0) + return 0; + +- fd = open(path, O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_NOATIME|O_NOFOLLOW); +- if (fd < 0) { +- if (errno == ELOOP) +- return log_error_errno(errno, "Skipping file attributes adjustment on symlink %s.", path); +- +- return log_error_errno(errno, "Cannot open '%s': %m", path); +- } +- +- if (fstat(fd, &st) < 0) +- return log_error_errno(errno, "Cannot stat '%s': %m", path); ++ r = fd_get_path(fd, &path); ++ if (r < 0) ++ return r; + + /* Issuing the file attribute ioctls on device nodes is not + * safe, as that will be delivered to the drivers, not the + * file system containing the device node. */ +- if (!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode)) { ++ if (!S_ISREG(st->st_mode) && !S_ISDIR(st->st_mode)) { + log_error("Setting file flags is only supported on regular files and directories, cannot set on '%s'.", path); + return -EINVAL; + } +@@ -1174,10 +1220,16 @@ static int path_set_attribute(Item *item, const char *path) { + f = item->attribute_value & item->attribute_mask; + + /* Mask away directory-specific flags */ +- if (!S_ISDIR(st.st_mode)) ++ if (!S_ISDIR(st->st_mode)) + f &= ~FS_DIRSYNC_FL; + +- r = chattr_fd(fd, f, item->attribute_mask); ++ xsprintf(procfs_path, "/proc/self/fd/%i", fd); ++ ++ procfs_fd = open(procfs_path, O_RDONLY|O_CLOEXEC|O_NOATIME); ++ if (procfs_fd < 0) ++ return -errno; ++ ++ r = chattr_fd(procfs_fd, f, item->attribute_mask); + if (r < 0) + log_full_errno(IN_SET(r, -ENOTTY, -EOPNOTSUPP) ? LOG_DEBUG : LOG_WARNING, + r, +@@ -1187,6 +1239,23 @@ static int path_set_attribute(Item *item, const char *path) { + return 0; + } + ++static int path_set_attribute(Item *item, const char *path) { ++ _cleanup_close_ int fd = -1; ++ struct stat st; ++ ++ if (!item->attribute_set || item->attribute_mask == 0) ++ return 0; ++ ++ fd = open(path, O_CLOEXEC|O_NOFOLLOW|O_PATH); ++ if (fd < 0) ++ return log_error_errno(errno, "Cannot open '%s': %m", path); ++ ++ if (fstat(fd, &st) < 0) ++ return log_error_errno(errno, "Cannot stat '%s': %m", path); ++ ++ return fd_set_attribute(item, fd, &st); ++} ++ + static int write_one_file(Item *i, const char *path) { + _cleanup_close_ int fd = -1; + int flags, r = 0; +@@ -1251,48 +1320,58 @@ done: + } + + typedef int (*action_t)(Item *, const char *); ++typedef int (*fdaction_t)(Item *, int fd, const struct stat *st); + +-static int item_do_children(Item *i, const char *path, action_t action) { +- _cleanup_closedir_ DIR *d; +- struct dirent *de; +- int r = 0; ++static int item_do(Item *i, int fd, const struct stat *st, fdaction_t action) { ++ int r = 0, q; + + assert(i); +- assert(path); ++ assert(fd >= 0); ++ assert(st); + + /* This returns the first error we run into, but nevertheless + * tries to go on */ ++ r = action(i, fd, st); + +- d = opendir_nomod(path); +- if (!d) +- return IN_SET(errno, ENOENT, ENOTDIR, ELOOP) ? 0 : -errno; ++ if (S_ISDIR(st->st_mode)) { ++ char procfs_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(int)]; ++ _cleanup_closedir_ DIR *d = NULL; ++ struct dirent *de; + +- FOREACH_DIRENT_ALL(de, d, r = -errno) { +- _cleanup_free_ char *p = NULL; +- int q; ++ /* The passed 'fd' was opened with O_PATH. We need to convert ++ * it into a 'regular' fd before reading the directory content. */ ++ xsprintf(procfs_path, "/proc/self/fd/%i", fd); + +- if (dot_or_dot_dot(de->d_name)) +- continue; ++ d = opendir(procfs_path); ++ if (!d) { ++ r = r ?: -errno; ++ goto finish; ++ } + +- p = strjoin(path, "/", de->d_name); +- if (!p) +- return -ENOMEM; ++ FOREACH_DIRENT_ALL(de, d, q = -errno; goto finish) { ++ struct stat de_st; ++ int de_fd; ++ ++ if (dot_or_dot_dot(de->d_name)) ++ continue; + +- q = action(i, p); +- if (q < 0 && q != -ENOENT && r == 0) +- r = q; ++ de_fd = openat(fd, de->d_name, O_NOFOLLOW|O_CLOEXEC|O_PATH); ++ if (de_fd >= 0 && fstat(de_fd, &de_st) >= 0) ++ /* pass ownership of dirent fd over */ ++ q = item_do(i, de_fd, &de_st, action); ++ else ++ q = -errno; + +- if (IN_SET(de->d_type, DT_UNKNOWN, DT_DIR)) { +- q = item_do_children(i, p, action); + if (q < 0 && r == 0) + r = q; + } + } +- ++finish: ++ safe_close(fd); + return r; + } + +-static int glob_item(Item *i, action_t action, bool recursive) { ++static int glob_item(Item *i, action_t action) { + _cleanup_globfree_ glob_t g = { + #ifdef GLOB_ALTDIRFUNC + .gl_opendir = (void *(*)(const char *)) opendir_nomod, +@@ -1309,12 +1388,48 @@ static int glob_item(Item *i, action_t action, bool recursive) { + k = action(i, *fn); + if (k < 0 && r == 0) + r = k; ++ } + +- if (recursive) { +- k = item_do_children(i, *fn, action); +- if (k < 0 && r == 0) +- r = k; ++ return r; ++} ++ ++static int glob_item_recursively(Item *i, fdaction_t action) { ++ _cleanup_globfree_ glob_t g = { ++ .gl_opendir = (void *(*)(const char *)) opendir_nomod, ++ }; ++ int r = 0, k; ++ char **fn; ++ ++ k = safe_glob(i->path, GLOB_NOSORT|GLOB_BRACE, &g); ++ if (k < 0 && k != -ENOENT) ++ return log_error_errno(k, "glob(%s) failed: %m", i->path); ++ ++ STRV_FOREACH(fn, g.gl_pathv) { ++ _cleanup_close_ int fd = -1; ++ struct stat st; ++ ++ /* Make sure we won't trigger/follow file object (such as ++ * device nodes, automounts, ...) pointed out by 'fn' with ++ * O_PATH. Note, when O_PATH is used, flags other than ++ * O_CLOEXEC, O_DIRECTORY, and O_NOFOLLOW are ignored. */ ++ ++ fd = open(*fn, O_CLOEXEC|O_NOFOLLOW|O_PATH); ++ if (fd < 0) { ++ r = r ?: -errno; ++ continue; ++ } ++ ++ if (fstat(fd, &st) < 0) { ++ r = r ?: -errno; ++ continue; + } ++ ++ k = item_do(i, fd, &st, action); ++ if (k < 0 && r == 0) ++ r = k; ++ ++ /* we passed fd ownership to the previous call */ ++ fd = -1; + } + + return r; +@@ -1403,7 +1518,7 @@ static int create_item(Item *i) { + break; + + case WRITE_FILE: +- r = glob_item(i, write_one_file, false); ++ r = glob_item(i, write_one_file); + if (r < 0) + return r; + +@@ -1662,49 +1777,49 @@ static int create_item(Item *i) { + + case ADJUST_MODE: + case RELABEL_PATH: +- r = glob_item(i, path_set_perms, false); ++ r = glob_item(i, path_set_perms); + if (r < 0) + return r; + break; + + case RECURSIVE_RELABEL_PATH: +- r = glob_item(i, path_set_perms, true); ++ r = glob_item_recursively(i, fd_set_perms); + if (r < 0) + return r; + break; + + case SET_XATTR: +- r = glob_item(i, path_set_xattrs, false); ++ r = glob_item(i, path_set_xattrs); + if (r < 0) + return r; + break; + + case RECURSIVE_SET_XATTR: +- r = glob_item(i, path_set_xattrs, true); ++ r = glob_item_recursively(i, fd_set_xattrs); + if (r < 0) + return r; + break; + + case SET_ACL: +- r = glob_item(i, path_set_acls, false); ++ r = glob_item(i, path_set_acls); + if (r < 0) + return r; + break; + + case RECURSIVE_SET_ACL: +- r = glob_item(i, path_set_acls, true); ++ r = glob_item_recursively(i, fd_set_acls); + if (r < 0) + return r; + break; + + case SET_ATTRIBUTE: +- r = glob_item(i, path_set_attribute, false); ++ r = glob_item(i, path_set_attribute); + if (r < 0) + return r; + break; + + case RECURSIVE_SET_ATTRIBUTE: +- r = glob_item(i, path_set_attribute, true); ++ r = glob_item_recursively(i, fd_set_attribute); + if (r < 0) + return r; + break; +@@ -1754,7 +1869,7 @@ static int remove_item(Item *i) { + case REMOVE_PATH: + case TRUNCATE_DIRECTORY: + case RECURSIVE_REMOVE_PATH: +- return glob_item(i, remove_item_instance, false); ++ return glob_item(i, remove_item_instance); + + default: + return 0; +@@ -1828,7 +1943,7 @@ static int clean_item(Item *i) { + return 0; + case EMPTY_DIRECTORY: + case IGNORE_DIRECTORY_PATH: +- return glob_item(i, clean_item_instance, false); ++ return glob_item(i, clean_item_instance); + default: + return 0; + } +-- +2.11.0 + -- cgit 1.2.3-korg