#define _POSIX_C_SOURCE 200809L #include "extract.h" #include "package.h" #include "verify.h" #include "stage.h" #include #include #include #include #include #include #include #include #include #include struct pending_link { char *name; char *target; }; struct archive_path { char *name; int blocks_children; }; static int compare_paths(const void *left, const void *right) { const struct archive_path *a = left, *b = right; return strcmp(a->name, b->name); } static int path_conflicts(struct archive_path *paths, size_t count) { size_t i; qsort(paths, count, sizeof *paths, compare_paths); for (i = 0; i < count; ++i) { size_t low, high, len; char *prefix; if (i && !strcmp(paths[i - 1].name, paths[i].name)) return 1; if (!paths[i].blocks_children) continue; len = strlen(paths[i].name); if (len > (size_t)-1 - 2) return 1; prefix = malloc(len + 2); if (!prefix) return 1; memcpy(prefix, paths[i].name, len); prefix[len] = '/'; prefix[len + 1] = '\0'; low = 0; high = count; while (low < high) { size_t middle = low + (high - low) / 2; if (strcmp(paths[middle].name, prefix) < 0) low = middle + 1; else high = middle; } free(prefix); if (low < count && !strncmp(paths[low].name, paths[i].name, len) && paths[low].name[len] == '/') return 1; } return 0; } static struct archive *reader(const char *source) { struct archive *a = archive_read_new(); if (!a) return NULL; if (archive_read_support_filter_lz4(a) != ARCHIVE_OK || archive_read_support_format_tar(a) != ARCHIVE_OK || archive_read_open_filename(a, source, 8192) != ARCHIVE_OK) { archive_read_free(a); return NULL; } return a; } static int supported(struct archive_entry *entry) { const char *name = archive_entry_pathname(entry); const char *target = archive_entry_hardlink(entry); mode_t type = archive_entry_filetype(entry); if (!holy_safe_archive_path(name) || archive_entry_size(entry) < 0 || archive_entry_xattr_count(entry) > 0 || archive_entry_acl_types(entry)) return 0; if (target) { if ((type != AE_IFREG && type != 0) || strncmp(name, "DATA/", 5) || !name[5] || !holy_safe_archive_path(target) || strncmp(target, "DATA/", 5) || !target[5] || archive_entry_size(entry) != 0) return 0; } else if (type == AE_IFLNK) { const char *destination = archive_entry_symlink(entry); if (strncmp(name, "DATA/", 5) || !name[5] || !destination || !*destination || destination[0] == '/' || archive_entry_size(entry) != 0) return 0; } else if (type != AE_IFDIR && type != AE_IFREG) return 0; if (type == AE_IFDIR && archive_entry_size(entry) != 0) return 0; if (type != AE_IFDIR && name[strlen(name) - 1] == '/') return 0; return 1; } int holy_extract_preflight(const char *source) { struct archive *a = reader(source); struct archive_entry *entry; struct archive_path *paths = NULL; size_t count = 0, i; int status, ok = 1; if (!a) { fprintf(stderr, "holypkg: archive open failed\n"); return 0; } while ((status = archive_read_next_header(a, &entry)) == ARCHIVE_OK) { struct archive_path *next; size_t length; if (!supported(entry)) { ok = 0; break; } if (count == (size_t)-1 / sizeof *paths) { ok = 0; break; } next = realloc(paths, (count + 1) * sizeof *paths); if (!next) { ok = 0; break; } paths = next; paths[count].name = strdup(archive_entry_pathname(entry)); if (!paths[count].name) { ok = 0; break; } length = strlen(paths[count].name); if (length > 1 && paths[count].name[length - 1] == '/') paths[count].name[length - 1] = '\0'; paths[count].blocks_children = archive_entry_filetype(entry) != AE_IFDIR; ++count; if (archive_read_data_skip(a) != ARCHIVE_OK) { ok = 0; break; } } if (status != ARCHIVE_EOF || (ok && path_conflicts(paths, count))) ok = 0; for (i = 0; i < count; ++i) free(paths[i].name); free(paths); archive_read_free(a); if (!ok) fprintf(stderr, "holypkg: unsupported or damaged extraction input\n"); return ok; } static int directory(int root, const char *path, int create) { char *parts = strdup(path), *cursor, *component, *slash; int current = root, next, ok = 0; if (!parts) return -1; cursor = parts; while (*cursor) { while (*cursor == '/') ++cursor; if (!*cursor) break; component = cursor; slash = strchr(cursor, '/'); if (slash) *slash = '\0'; if (create && mkdirat(current, component, 0700) && errno != EEXIST) goto done; next = openat(current, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (next < 0) goto done; if (current != root) close(current); current = next; cursor = slash ? slash + 1 : component + strlen(component); } ok = 1; done: free(parts); if (!ok && current != root) close(current); return ok ? current : -1; } static int write_file(int root, const char *name, struct archive *a, struct archive_entry *entry) { char *path = strdup(name), *last; char buffer[65536]; int parent = root, fd = -1, ok = 0; la_ssize_t got; la_int64_t total = 0; if (!path) return 0; last = strrchr(path, '/'); if (last) { *last++ = '\0'; parent = directory(root, path, 1); } else last = path; if (parent < 0 || !*last) goto done; fd = openat(parent, last, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); if (fd < 0) goto done; while ((got = archive_read_data(a, buffer, sizeof buffer)) > 0) { size_t written = 0; if (total > archive_entry_size(entry) - got) goto done; total += got; while (written < (size_t)got) { ssize_t sent = write(fd, buffer + written, (size_t)got - written); if (sent < 0 && errno == EINTR) continue; if (sent <= 0) goto done; written += (size_t)sent; } } if (got < 0 || total != archive_entry_size(entry) || fchmod(fd, archive_entry_perm(entry) & 0777) || fsync(fd)) goto done; ok = 1; done: if (fd >= 0) close(fd); if (parent != root && parent >= 0) close(parent); free(path); return ok; } static int parent_for(int root, const char *name, int create, char **storage, const char **base) { char *slash; int parent; *storage = strdup(name); if (!*storage) return -1; slash = strrchr(*storage, '/'); if (!slash || !slash[1]) { free(*storage); *storage = NULL; return -1; } *slash++ = '\0'; parent = directory(root, *storage, create); if (parent < 0) { free(*storage); *storage = NULL; return -1; } *base = slash; return parent; } static int write_symlink(int root, const char *name, const char *target) { char *path = NULL; const char *base; int parent = parent_for(root, name, 1, &path, &base), ok = 0; if (parent < 0) return 0; ok = symlinkat(target, parent, base) == 0; if (parent != root) close(parent); free(path); return ok; } static int write_hardlink(int root, const char *name, const char *target) { char *from = NULL, *to = NULL; const char *source_name, *dest_name; struct stat st; int source_parent, dest_parent = -1, ok = 0; source_parent = parent_for(root, target, 0, &from, &source_name); if (source_parent < 0) return 0; dest_parent = parent_for(root, name, 1, &to, &dest_name); if (dest_parent >= 0 && !fstatat(source_parent, source_name, &st, AT_SYMLINK_NOFOLLOW) && S_ISREG(st.st_mode) && !linkat(source_parent, source_name, dest_parent, dest_name, 0)) ok = 1; if (source_parent != root) close(source_parent); if (dest_parent >= 0 && dest_parent != root) close(dest_parent); free(from); free(to); return ok; } int holy_extract_local(const char *source, const char *output) { struct archive *a = NULL; struct archive_entry *entry; struct pending_link *links = NULL; char *snapshot = holy_stage_local(source, "holy-extract"); size_t link_count = 0, i; int root = -1, status, ok = 0, created = 0; if (!snapshot) { fprintf(stderr, "holypkg: could not stage regular local input\n"); return 0; } if (!holy_verify(snapshot) || !holy_extract_preflight(snapshot)) goto done; if (mkdir(output, 0700)) { perror("holypkg: create extraction directory"); goto done; } created = 1; root = open(output, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); a = reader(snapshot); if (root < 0 || !a) goto done; while ((status = archive_read_next_header(a, &entry)) == ARCHIVE_OK) { const char *name = archive_entry_pathname(entry); const char *target = archive_entry_hardlink(entry); int parent; if (!supported(entry)) goto done; if (target) { struct pending_link *next; if (link_count == (size_t)-1 / sizeof *links) goto done; next = realloc(links, (link_count + 1) * sizeof *links); if (!next) goto done; links = next; links[link_count].name = strdup(name); links[link_count].target = strdup(target); if (!links[link_count].name || !links[link_count].target) { free(links[link_count].name); free(links[link_count].target); goto done; } ++link_count; if (archive_read_data_skip(a) != ARCHIVE_OK) goto done; } else if (archive_entry_filetype(entry) == AE_IFLNK) { if (!write_symlink(root, name, archive_entry_symlink(entry)) || archive_read_data_skip(a) != ARCHIVE_OK) goto done; } else if (archive_entry_filetype(entry) == AE_IFDIR) { parent = directory(root, name, 1); if (parent < 0) goto done; if (parent != root) close(parent); } else if (!write_file(root, name, a, entry)) goto done; } if (status != ARCHIVE_EOF) goto done; for (i = 0; i < link_count; ++i) if (!write_hardlink(root, links[i].name, links[i].target)) goto done; if (fsync(root)) goto done; printf("extracted %s\n", output); ok = 1; done: if (!ok && created) fprintf(stderr, "holypkg: extraction failed; inspect partial output: %s\n", output); if (a) archive_read_free(a); if (root >= 0) close(root); for (i = 0; i < link_count; ++i) { free(links[i].name); free(links[i].target); } free(links); unlink(snapshot); free(snapshot); return ok; }