/* find device node of device with highest priority */ static const char *link_find_prioritized(struct udev_device *dev, bool add, const char *stackdir, char *buf, size_t bufsize) { struct udev *udev = udev_device_get_udev(dev); DIR *dir; int priority = 0; const char *target = NULL; if (add) { priority = udev_device_get_devlink_priority(dev); util_strscpy(buf, bufsize, udev_device_get_devnode(dev)); target = buf; } dir = opendir(stackdir); if (dir == NULL) return target; for (;;) { struct udev_device *dev_db; struct dirent *dent; dent = readdir(dir); if (dent == NULL || dent->d_name[0] == '\0') break; if (dent->d_name[0] == '.') continue; info(udev, "found '%s' claiming '%s'\n", dent->d_name, stackdir); /* did we find ourself? */ if (strcmp(dent->d_name, udev_device_get_id_filename(dev)) == 0) continue; dev_db = udev_device_new_from_device_id(udev, dent->d_name); if (dev_db != NULL) { const char *devnode; devnode = udev_device_get_devnode(dev_db); if (devnode != NULL) { dbg(udev, "compare priority of '%s'(%i) > '%s'(%i)\n", target, priority, udev_device_get_devnode(dev_db), udev_device_get_devlink_priority(dev_db)); if (target == NULL || udev_device_get_devlink_priority(dev_db) > priority) { info(udev, "'%s' claims priority %i for '%s'\n", udev_device_get_syspath(dev_db), udev_device_get_devlink_priority(dev_db), stackdir); priority = udev_device_get_devlink_priority(dev_db); util_strscpy(buf, bufsize, devnode); target = buf; } } udev_device_unref(dev_db); } } closedir(dir); return target; }
static void set_scsi_type(char *to, const char *from, size_t len) { int type_num; char *eptr; const char *type = "generic"; type_num = strtoul(from, &eptr, 0); if (eptr != from) { switch (type_num) { case 0: case 0xe: type = "disk"; break; case 1: type = "tape"; break; case 4: case 7: case 0xf: type = "optical"; break; case 5: type = "cd"; break; default: break; } } util_strscpy(to, len, type); }
static int set_usb_mass_storage_ifsubtype(char *to, const char *from, size_t len) { int type_num = 0; char *eptr; const char *type = "generic"; type_num = strtoul(from, &eptr, 0); if (eptr != from) { switch (type_num) { case 2: type = "atapi"; break; case 3: type = "tape"; break; case 4: /* UFI */ case 5: /* SFF-8070i */ type = "floppy"; break; case 1: /* RBC devices */ type = "rbc"; break; case 6: /* Transparent SPC-2 devices */ type = "scsi"; break; default: break; } } util_strscpy(to, len, type); return type_num; }
int util_delete_path(struct udev *udev, const char *path) { char p[UTIL_PATH_SIZE]; char *pos; int err = 0; if (path[0] == '/') while(path[1] == '/') path++; util_strscpy(p, sizeof(p), path); pos = strrchr(p, '/'); if (pos == p || pos == NULL) return 0; for (;;) { *pos = '\0'; pos = strrchr(p, '/'); /* don't remove the last one */ if ((pos == p) || (pos == NULL)) break; err = rmdir(p); if (err < 0) { if (errno == ENOENT) err = 0; break; } } return err; }
int udev_builtin_run(struct udev_device *dev, enum udev_builtin_cmd cmd, const char *command, bool test) { char arg[UTIL_PATH_SIZE]; int argc; char *argv[128]; optind = 0; util_strscpy(arg, sizeof(arg), command); udev_build_argv(udev_device_get_udev(dev), arg, &argc, argv); return builtins[cmd]->cmd(dev, argc, argv, test); }
static int names_usb(struct udev_device *dev, struct netnames *names) { struct udev_device *usbdev; char name[256]; char *ports; char *config; char *interf; size_t l; char *s; usbdev = udev_device_get_parent_with_subsystem_devtype(dev, "usb", "usb_interface"); if (!usbdev) return -ENOENT; /* get USB port number chain, configuration, interface */ util_strscpy(name, sizeof(name), udev_device_get_sysname(usbdev)); s = strchr(name, '-'); if (!s) return -EINVAL; ports = s+1; s = strchr(ports, ':'); if (!s) return -EINVAL; s[0] = '\0'; config = s+1; s = strchr(config, '.'); if (!s) return -EINVAL; s[0] = '\0'; interf = s+1; /* prefix every port number in the chain with "u"*/ s = ports; while ((s = strchr(s, '.'))) s[0] = 'u'; s = names->usb_ports; l = util_strpcpyl(&s, sizeof(names->usb_ports), "u", ports, NULL); /* append USB config number, suppress the common config == 1 */ if (!streq(config, "1")) l = util_strpcpyl(&s, sizeof(names->usb_ports), "c", config, NULL); /* append USB interface number, suppress the interface == 0 */ if (!streq(interf, "0")) l = util_strpcpyl(&s, sizeof(names->usb_ports), "i", interf, NULL); if (l == 0) return -ENAMETOOLONG; names->type = NET_USB; return 0; }
enum udev_builtin_cmd udev_builtin_lookup(const char *command) { char name[UTIL_PATH_SIZE]; enum udev_builtin_cmd i; char *pos; util_strscpy(name, sizeof(name), command); pos = strchr(name, ' '); if (pos) pos[0] = '\0'; for (i = 0; i < ELEMENTSOF(builtins); i++) if (strcmp(builtins[i]->name, name) == 0) return i; return UDEV_BUILTIN_MAX; }
static int create_path(struct udev *udev, const char *path, bool selinux) { char p[UTIL_PATH_SIZE]; char *pos; struct stat stats; int err; util_strscpy(p, sizeof(p), path); pos = strrchr(p, '/'); if (pos == NULL) return 0; while (pos != p && pos[-1] == '/') pos--; if (pos == p) return 0; pos[0] = '\0'; dbg(udev, "stat '%s'\n", p); if (stat(p, &stats) == 0) { if ((stats.st_mode & S_IFMT) == S_IFDIR) return 0; else return -ENOTDIR; } err = util_create_path(udev, p); if (err != 0) return err; dbg(udev, "mkdir '%s'\n", p); if (selinux) udev_selinux_setfscreatecon(udev, p, S_IFDIR|0755); err = mkdir(p, 0755); if (err != 0) { err = -errno; if (err == -EEXIST && stat(p, &stats) == 0) { if ((stats.st_mode & S_IFMT) == S_IFDIR) err = 0; else err = -ENOTDIR; } } if (selinux) udev_selinux_resetfscreatecon(udev); return err; }
/** * udev_monitor_new_from_socket: * @udev: udev library context * @socket_path: unix socket path * * This function should not be used in any new application. The * kernel's netlink socket multiplexes messages to all interested * clients. Creating custom sockets from udev to applications * should be avoided. * * Create a new udev monitor and connect to a specified socket. The * path to a socket either points to an existing socket file, or if * the socket path starts with a '@' character, an abstract namespace * socket will be used. * * A socket file will not be created. If it does not already exist, * it will fall-back and connect to an abstract namespace socket with * the given path. The permissions adjustment of a socket file, as * well as the later cleanup, needs to be done by the caller. * * The initial refcount is 1, and needs to be decremented to * release the resources of the udev monitor. * * Returns: a new udev monitor, or #NULL, in case of an error **/ UDEV_EXPORT struct udev_monitor *udev_monitor_new_from_socket(struct udev *udev, const char *socket_path) { struct udev_monitor *udev_monitor; struct stat statbuf; if (udev == NULL) return NULL; if (socket_path == NULL) return NULL; udev_monitor = udev_monitor_new(udev); if (udev_monitor == NULL) return NULL; udev_monitor->sun.sun_family = AF_LOCAL; if (socket_path[0] == '@') { /* translate leading '@' to abstract namespace */ util_strscpy(udev_monitor->sun.sun_path, sizeof(udev_monitor->sun.sun_path), socket_path); udev_monitor->sun.sun_path[0] = '\0'; udev_monitor->addrlen = offsetof(struct sockaddr_un, sun_path) + strlen(socket_path); } else if (stat(socket_path, &statbuf) == 0 && S_ISSOCK(statbuf.st_mode)) {
static int builtin_btrfs(struct udev_device *dev, int argc, char *argv[], bool test) { struct btrfs_ioctl_vol_args args; int fd; int err; if (argc != 3 || !streq(argv[1], "ready")) return EXIT_FAILURE; fd = open("/dev/btrfs-control", O_RDWR); if (fd < 0) return EXIT_FAILURE; util_strscpy(args.name, sizeof(args.name), argv[2]); err = ioctl(fd, BTRFS_IOC_DEVICES_READY, &args); close(fd); if (err < 0) return EXIT_FAILURE; udev_builtin_add_property(dev, test, "ID_BTRFS_READY", err == 0 ? "1" : "0"); return EXIT_SUCCESS; }
static int node_symlink(struct udev *udev, const char *node, const char *slink) { struct stat stats; char target[UTIL_PATH_SIZE]; char *s; size_t l; char slink_tmp[UTIL_PATH_SIZE + sizeof(TMP_FILE_EXT)]; int i = 0; int tail = 0; int err = 0; /* use relative link */ target[0] = '\0'; while (node[i] && (node[i] == slink[i])) { if (node[i] == '/') tail = i+1; i++; } s = target; l = sizeof(target); while (slink[i] != '\0') { if (slink[i] == '/') l = util_strpcpy(&s, l, "../"); i++; } l = util_strscpy(s, l, &node[tail]); if (l == 0) { err = -EINVAL; goto exit; } /* preserve link with correct target, do not replace node of other device */ if (lstat(slink, &stats) == 0) { if (S_ISBLK(stats.st_mode) || S_ISCHR(stats.st_mode)) { struct stat stats2; info(udev, "found existing node instead of symlink '%s'\n", slink); if (lstat(node, &stats2) == 0) { if ((stats.st_mode & S_IFMT) == (stats2.st_mode & S_IFMT) && stats.st_rdev == stats2.st_rdev && stats.st_ino != stats2.st_ino) { info(udev, "replace device node '%s' with symlink to our node '%s'\n", slink, node); } else { err(udev, "device node '%s' already exists, " "link to '%s' will not overwrite it\n", slink, node); goto exit; } } } else if (S_ISLNK(stats.st_mode)) { char buf[UTIL_PATH_SIZE]; int len; dbg(udev, "found existing symlink '%s'\n", slink); len = readlink(slink, buf, sizeof(buf)); if (len > 0 && len < (int)sizeof(buf)) { buf[len] = '\0'; if (strcmp(target, buf) == 0) { info(udev, "preserve already existing symlink '%s' to '%s'\n", slink, target); udev_selinux_lsetfilecon(udev, slink, S_IFLNK); utimensat(AT_FDCWD, slink, NULL, AT_SYMLINK_NOFOLLOW); goto exit; } } } } else { info(udev, "creating symlink '%s' to '%s'\n", slink, target); do { err = util_create_path_selinux(udev, slink); if (err != 0 && err != -ENOENT) break; udev_selinux_setfscreatecon(udev, slink, S_IFLNK); err = symlink(target, slink); if (err != 0) err = -errno; udev_selinux_resetfscreatecon(udev); } while (err == -ENOENT); if (err == 0) goto exit; } info(udev, "atomically replace '%s'\n", slink); util_strscpyl(slink_tmp, sizeof(slink_tmp), slink, TMP_FILE_EXT, NULL); unlink(slink_tmp); do { err = util_create_path_selinux(udev, slink_tmp); if (err != 0 && err != -ENOENT) break; udev_selinux_setfscreatecon(udev, slink_tmp, S_IFLNK); err = symlink(target, slink_tmp); if (err != 0) err = -errno; udev_selinux_resetfscreatecon(udev); } while (err == -ENOENT); if (err != 0) { err(udev, "symlink '%s' '%s' failed: %m\n", target, slink_tmp); goto exit; } err = rename(slink_tmp, slink); if (err != 0) { err(udev, "rename '%s' '%s' failed: %m\n", slink_tmp, slink); unlink(slink_tmp); } exit: return err; }
/* find device node of device with highest priority */ static const char *link_find_prioritized(struct udev_device *dev, bool add, const char *stackdir, char *buf, size_t bufsize) { struct udev *udev = udev_device_get_udev(dev); DIR *dir; int priority = 0; const char *target = NULL; if (add) { priority = udev_device_get_devlink_priority(dev); util_strscpy(buf, bufsize, udev_device_get_devnode(dev)); target = buf; } dir = opendir(stackdir); if (dir == NULL) return target; for (;;) { struct udev_device *dev_db; struct dirent *dent; char devpath[UTIL_PATH_SIZE]; char syspath[UTIL_PATH_SIZE]; ssize_t len; dent = readdir(dir); if (dent == NULL || dent->d_name[0] == '\0') break; if (dent->d_name[0] == '.') continue; dbg(udev, "found '%s/%s'\n", stackdir, dent->d_name); len = readlinkat(dirfd(dir), dent->d_name, devpath, sizeof(devpath)); if (len <= 0 || len == (ssize_t)sizeof(devpath)) continue; devpath[len] = '\0'; util_strscpyl(syspath, sizeof(syspath), udev_get_sys_path(udev), devpath, NULL); info(udev, "found '%s' claiming '%s'\n", syspath, stackdir); /* did we find ourself? */ if (strcmp(udev_device_get_syspath(dev), syspath) == 0) continue; dev_db = udev_device_new_from_syspath(udev, syspath); if (dev_db != NULL) { const char *devnode; devnode = udev_device_get_devnode(dev_db); if (devnode != NULL) { dbg(udev, "compare priority of '%s'(%i) > '%s'(%i)\n", target, priority, udev_device_get_devnode(dev_db), udev_device_get_devlink_priority(dev_db)); if (target == NULL || udev_device_get_devlink_priority(dev_db) > priority) { info(udev, "'%s' claims priority %i for '%s'\n", syspath, udev_device_get_devlink_priority(dev_db), stackdir); priority = udev_device_get_devlink_priority(dev_db); util_strscpy(buf, bufsize, devnode); target = buf; } } udev_device_unref(dev_db); } } closedir(dir); return target; }
static int set_options(struct udev *udev, int argc, char **argv, const char *short_opts, char *maj_min_dev) { int option; /* * optind is a global extern used by getopt. Since we can call * set_options twice (once for command line, and once for config * file) we have to reset this back to 1. */ optind = 1; while (1) { option = getopt_long(argc, argv, short_opts, options, NULL); if (option == -1) break; switch (option) { case 'b': all_good = 0; break; case 'd': dev_specified = 1; util_strscpy(maj_min_dev, MAX_PATH_LEN, optarg); break; case 'e': use_stderr = 1; break; case 'f': util_strscpy(config_file, MAX_PATH_LEN, optarg); break; case 'g': all_good = 1; break; case 'h': printf("Usage: scsi_id OPTIONS <device>\n" " --device= device node for SG_IO commands\n" " --config= location of config file\n" " --page=0x80|0x83|pre-spc3-83 SCSI page (0x80, 0x83, pre-spc3-83)\n" " --sg-version=3|4 use SGv3 or SGv4\n" " --blacklisted threat device as blacklisted\n" " --whitelisted threat device as whitelisted\n" " --replace-whitespace replace all whitespaces by underscores\n" " --verbose verbose logging\n" " --version print version\n" " --export print values as environment keys\n" " --help print this help text\n\n"); exit(0); case 'p': if (strcmp(optarg, "0x80") == 0) { default_page_code = PAGE_80; } else if (strcmp(optarg, "0x83") == 0) { default_page_code = PAGE_83; } else if (strcmp(optarg, "pre-spc3-83") == 0) { default_page_code = PAGE_83_PRE_SPC3; } else { log_error("Unknown page code '%s'\n", optarg); return -1; } break; case 's': sg_version = atoi(optarg); if (sg_version < 3 || sg_version > 4) { log_error("Unknown SG version '%s'\n", optarg); return -1; } break; case 'u': reformat_serial = 1; break; case 'x': export = 1; break; case 'v': debug++; break; case 'V': printf("%s\n", VERSION); exit(0); break; default: exit(1); } } if (optind < argc && !dev_specified) { dev_specified = 1; util_strscpy(maj_min_dev, MAX_PATH_LEN, argv[optind]); } return 0; }
int main(int argc, char *argv[]) { struct udev *udev; static const struct option options[] = { { "export", no_argument, NULL, 'x' }, { "debug", no_argument, NULL, 'd' }, { "help", no_argument, NULL, 'h' }, {} }; char **devices; FILE *fp; struct mntent *mnt; int rc = 1; udev = udev_new(); if (udev == NULL) goto exit; udev_log_init("fstab_id"); udev_set_log_fn(udev, log_fn); while (1) { int option; option = getopt_long(argc, argv, "dxh", options, NULL); if (option == -1) break; switch (option) { case 'd': debug = 1; if (udev_get_log_priority(udev) < LOG_INFO) udev_set_log_priority(udev, LOG_INFO); break; case 'h': printf("Usage: fstab_id [OPTIONS] name [...]\n" " --export print environment keys\n" " --debug debug to stderr\n" " --help print this help text\n\n"); goto exit; case 'x': break; default: rc = 2; goto exit; } } devices = &argv[optind]; if (devices[0] == NULL) { fprintf(stderr, "error: missing device(s) to match\n"); rc = 3; goto exit; } fp = setmntent ("/etc/fstab", "r"); if (fp == NULL) { fprintf(stderr, "error: opening fstab: %s\n", strerror(errno)); rc = 4; goto exit; } while (1) { mnt = getmntent(fp); if (mnt == NULL) break; info(udev, "found '%s'@'%s'\n", mnt->mnt_fsname, mnt->mnt_dir); /* skip root device */ if (strcmp(mnt->mnt_dir, "/") == 0) continue; /* match LABEL */ if (strncmp(mnt->mnt_fsname, "LABEL=", 6) == 0) { const char *label; char str[256]; label = &mnt->mnt_fsname[6]; if (label[0] == '"' || label[0] == '\'') { char *pos; util_strscpy(str, sizeof(str), &label[1]); pos = strrchr(str, label[0]); if (pos == NULL) continue; pos[0] = '\0'; label = str; } if (matches_device_list(udev, devices, label)) { print_fstab_entry(udev, mnt); rc = 0; break; } continue; } /* match UUID */ if (strncmp(mnt->mnt_fsname, "UUID=", 5) == 0) { const char *uuid; char str[256]; uuid = &mnt->mnt_fsname[5]; if (uuid[0] == '"' || uuid[0] == '\'') { char *pos; util_strscpy(str, sizeof(str), &uuid[1]); pos = strrchr(str, uuid[0]); if (pos == NULL) continue; pos[0] = '\0'; uuid = str; } if (matches_device_list(udev, devices, uuid)) { print_fstab_entry(udev, mnt); rc = 0; break; } continue; } /* only devices */ if (strncmp(mnt->mnt_fsname, udev_get_dev_path(udev), strlen(udev_get_dev_path(udev))) != 0) continue; if (matches_device_list(udev, devices, &mnt->mnt_fsname[strlen(udev_get_dev_path(udev))+1])) { print_fstab_entry(udev, mnt); rc = 0; break; } } endmntent(fp); exit: udev_unref(udev); udev_log_close(); return rc; }
static int adm_builtin(struct udev *udev, int argc, char *argv[]) { static const struct option options[] = { { "help", no_argument, NULL, 'h' }, {} }; char *command = NULL; char *syspath = NULL; char filename[UTIL_PATH_SIZE]; struct udev_device *dev = NULL; enum udev_builtin_cmd cmd; int rc = EXIT_SUCCESS; dbg(udev, "version %s\n", VERSION); for (;;) { int option; option = getopt_long(argc, argv, "h", options, NULL); if (option == -1) break; switch (option) { case 'h': help(udev); goto out; } } command = argv[optind++]; if (command == NULL) { fprintf(stderr, "command missing\n"); help(udev); rc = 2; goto out; } syspath = argv[optind++]; if (syspath == NULL) { fprintf(stderr, "syspath missing\n\n"); rc = 3; goto out; } /* add /sys if needed */ if (strncmp(syspath, udev_get_sys_path(udev), strlen(udev_get_sys_path(udev))) != 0) util_strscpyl(filename, sizeof(filename), udev_get_sys_path(udev), syspath, NULL); else util_strscpy(filename, sizeof(filename), syspath); util_remove_trailing_chars(filename, '/'); dev = udev_device_new_from_syspath(udev, filename); if (dev == NULL) { fprintf(stderr, "unable to open device '%s'\n\n", filename); rc = 4; goto out; } cmd = udev_builtin_lookup(command); if (cmd >= UDEV_BUILTIN_MAX) { fprintf(stderr, "unknown command '%s'\n", command); help(udev); rc = 5; goto out; } if (udev_builtin_run(dev, cmd, true) < 0) { fprintf(stderr, "error executing '%s'\n\n", command); rc = 6; } out: udev_device_unref(dev); return rc; }
/* handle "[<SUBSYSTEM>/<KERNEL>]<attribute>" format */ int util_resolve_subsys_kernel(struct udev *udev, const char *string, char *result, size_t maxsize, int read_value) { char temp[UTIL_PATH_SIZE]; char *subsys; char *sysname; struct udev_device *dev; char *attr; if (string[0] != '[') return -1; util_strscpy(temp, sizeof(temp), string); subsys = &temp[1]; sysname = strchr(subsys, '/'); if (sysname == NULL) return -1; sysname[0] = '\0'; sysname = &sysname[1]; attr = strchr(sysname, ']'); if (attr == NULL) return -1; attr[0] = '\0'; attr = &attr[1]; if (attr[0] == '/') attr = &attr[1]; if (attr[0] == '\0') attr = NULL; if (read_value && attr == NULL) return -1; dev = udev_device_new_from_subsystem_sysname(udev, subsys, sysname); if (dev == NULL) return -1; if (read_value) { const char *val; val = udev_device_get_sysattr_value(dev, attr); if (val != NULL) util_strscpy(result, maxsize, val); else result[0] = '\0'; info(udev, "value '[%s/%s]%s' is '%s'\n", subsys, sysname, attr, result); } else { size_t l; char *s; s = result; l = util_strpcpyl(&s, maxsize, udev_device_get_syspath(dev), NULL); if (attr != NULL) util_strpcpyl(&s, l, "/", attr, NULL); info(udev, "path '[%s/%s]%s' is '%s'\n", subsys, sysname, attr, result); } udev_device_unref(dev); return 0; }
static int node_symlink(struct udev *udev, const char *node, const char *slink) { struct stat stats; char target[UTIL_PATH_SIZE]; char *s; size_t l; char slink_tmp[UTIL_PATH_SIZE + sizeof(TMP_FILE_EXT)]; int i = 0; int tail = 0; int err = 0; /* use relative link */ target[0] = '\0'; while (node[i] && (node[i] == slink[i])) { if (node[i] == '/') tail = i+1; i++; } s = target; l = sizeof(target); while (slink[i] != '\0') { if (slink[i] == '/') l = util_strpcpy(&s, l, "../"); i++; } l = util_strscpy(s, l, &node[tail]); if (l == 0) { err = -EINVAL; goto exit; } /* preserve link with correct target, do not replace node of other device */ if (lstat(slink, &stats) == 0) { if (S_ISBLK(stats.st_mode) || S_ISCHR(stats.st_mode)) { log_error("conflicting device node '%s' found, link to '%s' will not be created\n", slink, node); goto exit; } else if (S_ISLNK(stats.st_mode)) { char buf[UTIL_PATH_SIZE]; int len; len = readlink(slink, buf, sizeof(buf)); if (len > 0 && len < (int)sizeof(buf)) { buf[len] = '\0'; if (strcmp(target, buf) == 0) { log_debug("preserve already existing symlink '%s' to '%s'\n", slink, target); label_fix(slink, true, false); utimensat(AT_FDCWD, slink, NULL, AT_SYMLINK_NOFOLLOW); goto exit; } } } } else { log_debug("creating symlink '%s' to '%s'\n", slink, target); do { err = mkdir_parents_label(slink, 0755); if (err != 0 && err != -ENOENT) break; label_context_set(slink, S_IFLNK); err = symlink(target, slink); if (err != 0) err = -errno; label_context_clear(); } while (err == -ENOENT); if (err == 0) goto exit; } log_debug("atomically replace '%s'\n", slink); util_strscpyl(slink_tmp, sizeof(slink_tmp), slink, TMP_FILE_EXT, NULL); unlink(slink_tmp); do { err = mkdir_parents_label(slink_tmp, 0755); if (err != 0 && err != -ENOENT) break; label_context_set(slink_tmp, S_IFLNK); err = symlink(target, slink_tmp); if (err != 0) err = -errno; label_context_clear(); } while (err == -ENOENT); if (err != 0) { log_error("symlink '%s' '%s' failed: %m\n", target, slink_tmp); goto exit; } err = rename(slink_tmp, slink); if (err != 0) { log_error("rename '%s' '%s' failed: %m\n", slink_tmp, slink); unlink(slink_tmp); } exit: return err; }