#define _POSIX_C_SOURCE 200809L #include "resolve.h" #include "deps.h" #include "package.h" #include "provides.h" #include "scan.h" #include "solve.h" #include "stage.h" #include "verify.h" #include "../backends/pacman.h" #include "../backends/deb-version.h" #include "version.h" #include "../backends/apk-version.h" #include "../backends/xbps-version.h" #include "../backends/rpm-version.h" #include #include #include #include #include #include #include #include struct elf_edge { size_t requirement; const char *path, *kind, *target; char *owned_target; const struct holy_scanned_file *file; const struct holy_elf_symbol *symbol; }; struct package_edge { size_t requirement; char *kind, *name, *arch, *libc, *relation, *version; }; struct package_claim { char *capability, *version; }; struct payload_path { char *path, *link; unsigned int mode; }; struct version_adapter { const char *family; int (*compare)(const char *, const char *, int *); }; static const struct version_adapter version_adapters[] = { {"pacman", holy_pacman_version_compare}, {"deb", holy_deb_version_compare}, {"holy", holy_version_compare}, {"apk", holy_apk_version_compare}, {"xbps", holy_xbps_version_compare}, {"rpm", holy_rpm_version_compare} }; struct local_item { struct holy_package_identity identity; char *unsupported_id; struct holy_solver_requirement *requirements; char **requirement_ids; char **original_requirements; size_t requirement_count; char *capability; struct holy_scan_result scan; struct elf_edge *edges; size_t edge_count; struct package_edge *package_edges; size_t package_edge_count; struct package_claim *claims; size_t claim_count; struct payload_path *file_paths; size_t file_count; }; static int literal_path(const char *path); static char *named_capability(const char *kind, const char *name) { size_t prefix = strlen(kind), n = strlen(name); char *capability; if (prefix > 65536 || n > 65536 - prefix - 2) return NULL; capability = malloc(prefix + n + 2); if (capability) { memcpy(capability, kind, prefix); capability[prefix] = ':'; memcpy(capability + prefix + 1, name, n + 1); } return capability; } static char *package_capability(const char *name) { return named_capability("package", name); } static int add_requirement(struct local_item *item, const char *id, const char *cap) { struct holy_solver_requirement *next; char **ids; char *capability; char *identifier; char *original; size_t i; if (item->requirement_count >= 65536) return 0; for (i = 0; i < item->requirement_count; ++i) if (!strcmp(item->requirement_ids[i], id)) return 0; capability = strdup(cap); if (!capability) return 0; identifier = strdup(id); if (!identifier) { free(capability); return 0; } original = strdup(cap); if (!original) { free(capability); free(identifier); return 0; } next = realloc(item->requirements, (item->requirement_count + 1) * sizeof *next); if (!next) { free(identifier); free(capability); free(original); return 0; } item->requirements = next; ids = realloc(item->requirement_ids, (item->requirement_count + 1) * sizeof *ids); if (!ids) { free(identifier); free(capability); free(original); return 0; } item->requirement_ids = ids; ids = realloc(item->original_requirements, (item->requirement_count + 1) * sizeof *ids); if (!ids) { free(identifier); free(capability); free(original); return 0; } item->original_requirements = ids; ids[item->requirement_count] = original; item->requirements[item->requirement_count].first = capability; item->requirements[item->requirement_count].alternative = NULL; item->requirement_ids[item->requirement_count] = identifier; ++item->requirement_count; return 1; } static int add_package_edge(struct local_item *item, size_t requirement, const char *kind, const char *name, const char *arch, const char *libc, const char *relation, const char *version) { struct package_edge *grown, *edge; if (item->package_edge_count >= 65536) return 0; grown = realloc(item->package_edges, (item->package_edge_count + 1) * sizeof *grown); if (!grown) return 0; item->package_edges = grown; edge = &grown[item->package_edge_count++]; memset(edge, 0, sizeof *edge); edge->requirement = requirement; edge->kind = strdup(kind); edge->name = strdup(name); edge->arch = strdup(arch); edge->libc = strdup(libc); edge->relation = strdup(relation); edge->version = strdup(version); return edge->kind && edge->name && edge->arch && edge->libc && edge->relation && edge->version; } struct or_context { struct local_item *item; size_t requirement; }; static int add_or_branch(void *opaque, const char *name, const char *relation, const char *version) { struct or_context *context = opaque; return add_package_edge(context->item, context->requirement, "package", name, "any", "any", relation, version); } static int exact_requirement(void *opaque, const char *id, const char *consumer, const char *kind, const char *name, const char *arch, const char *libc, const char *relation, const char *version, const char *original, const char *evidence) { struct local_item *item = opaque; char *capability; int ok; (void)original; (void)evidence; if (strcmp(consumer, item->identity.name)) return 0; if (!strcmp(kind, "package-or")) { struct or_context context; if (!item->identity.version_family || strcmp(item->identity.version_family, "deb") || strcmp(relation, "any") || strcmp(arch, "any") || strcmp(libc, "any")) return 0; capability = named_capability(kind, name); if (!capability) return 0; ok = add_requirement(item, id, capability); free(capability); if (!ok) return 0; context.item = item; context.requirement = item->requirement_count - 1; return holy_package_or_each(name, add_or_branch, &context); } if (strcmp(kind, "package") && strcmp(kind, "file") && strcmp(kind, "command") && strcmp(kind, "soname")) { if (!item->unsupported_id) item->unsupported_id = strdup(id); return item->unsupported_id != NULL; } if (!strcmp(kind, "soname") && (!name[0] || strchr(name, '/') || strcmp(relation, "any"))) { if (!item->unsupported_id) item->unsupported_id = strdup(id); return item->unsupported_id != NULL; } if ( (!strcmp(kind, "file") && (!literal_path(name) || strcmp(relation, "any"))) || (!strcmp(kind, "command") && (!name[0] || strchr(name, '/') || !strcmp(name, ".") || !strcmp(name, "..") || strcmp(relation, "any")))) return 0; capability = named_capability(kind, name); if (!capability) return 0; ok = add_requirement(item, id, capability); free(capability); if (ok && (strcmp(kind, "package") || strcmp(arch, "any") || strcmp(libc, "any") || strcmp(relation, "any"))) { ok = add_package_edge(item, item->requirement_count - 1, kind, name, arch, libc, relation, version); } return ok; } static int package_claim(void *opaque, const char *kind, const char *name, const char *arch, const char *libc, const char *version, const char *evidence) { struct local_item *item = opaque; struct package_claim *grown, *claim; (void)evidence; if (strcmp(kind, "package")) return 1; if ((strcmp(arch, "any") && strcmp(arch, item->identity.arch)) || (strcmp(libc, "any") && strcmp(libc, item->identity.libc))) { fprintf(stderr, "holypkg: package capability scope disagrees with artifact: %s\n", name); return 0; } if (item->claim_count == 65536) return 0; grown = realloc(item->claims, (item->claim_count + 1) * sizeof *grown); if (!grown) return 0; item->claims = grown; claim = &grown[item->claim_count++]; claim->capability = package_capability(name); claim->version = strdup(version); return claim->capability && claim->version; } static const struct version_adapter *version_adapter(const char *family) { size_t i; if (!family) return NULL; for (i = 0; i < sizeof version_adapters / sizeof *version_adapters; ++i) if (!strcmp(family, version_adapters[i].family)) return &version_adapters[i]; return NULL; } static int version_matches(const char *candidate, const struct package_edge *edge, const struct version_adapter *adapter) { int order; if (!strcmp(edge->relation, "any")) return 1; if (!strcmp(candidate, "-")) return 0; if (!adapter || !adapter->compare(candidate, edge->version, &order)) return -1; return !strcmp(edge->relation, "eq") ? order == 0 : !strcmp(edge->relation, "ge") ? order >= 0 : !strcmp(edge->relation, "gt") ? order > 0 : !strcmp(edge->relation, "le") ? order <= 0 : order < 0; } static int has_file(const struct local_item *item, const char *absolute); static int has_command(const struct local_item *item, const char *name); static int has_soname(const struct local_item *item, const char *name); static int package_edge_matches(const struct local_item *consumer, const struct package_edge *edge, const struct local_item *candidate) { const char *family = consumer->identity.version_family; const struct version_adapter *adapter = version_adapter(family); char *base; int matches = 0, constrained = strcmp(edge->relation, "any") != 0; size_t claim; if ((strcmp(edge->arch, "any") && strcmp(edge->arch, candidate->identity.arch)) || (strcmp(edge->libc, "any") && strcmp(edge->libc, candidate->identity.libc)) || (constrained && (!candidate->identity.version_family || strcmp(candidate->identity.version_family, family)))) return 0; if (!strcmp(edge->kind, "file")) return has_file(candidate, edge->name); if (!strcmp(edge->kind, "command")) return has_command(candidate, edge->name); if (!strcmp(edge->kind, "soname")) return has_soname(candidate, edge->name); base = named_capability(edge->kind, edge->name); if (!base) return -1; if (!strcmp(base, candidate->capability)) { const char *candidate_version = candidate->identity.version; char *with_revision = NULL; if (constrained && (!strcmp(family, "xbps") || !strcmp(family, "rpm")) && candidate->identity.release) { size_t length = strlen(candidate_version) + strlen(candidate->identity.release) + 2; with_revision = malloc(length); if (!with_revision) { free(base); return -1; } snprintf(with_revision, length, "%s%c%s", candidate_version, !strcmp(family, "rpm") ? '-' : '_', candidate->identity.release); candidate_version = with_revision; } matches = version_matches(candidate_version, edge, adapter); free(with_revision); } for (claim = 0; !matches && claim < candidate->claim_count; ++claim) if (!strcmp(base, candidate->claims[claim].capability)) matches = version_matches(candidate->claims[claim].version, edge, adapter); free(base); return matches; } static int add_provide(struct holy_solver_item *item, const char *capability) { const char **next; char *copy; size_t i; for (i = 0; i < item->provides_count; ++i) if (!strcmp(item->provides[i], capability)) return 1; if (item->provides_count >= 65536) return 0; copy = strdup(capability); if (!copy) return 0; next = realloc((void *)item->provides, (item->provides_count + 1) * sizeof *next); if (!next) { free(copy); return 0; } item->provides = next; next[item->provides_count++] = copy; return 1; } static int file_path_order(const void *left, const void *right) { const struct payload_path *a = left, *b = right; return strcmp(a->path, b->path); } static int collect_file_path(void *opaque, const struct holy_manifest_entry *entry) { struct local_item *item = opaque; struct payload_path *paths, *path; if (entry->directory) return 1; if (item->file_count == SIZE_MAX / sizeof *paths) return 0; paths = realloc(item->file_paths, (item->file_count + 1) * sizeof *paths); if (!paths) return 0; item->file_paths = paths; path = &paths[item->file_count]; path->path = strdup(entry->path); path->link = entry->link ? strdup(entry->link) : NULL; path->mode = entry->mode; if (!path->path || (entry->link && !path->link)) { free(path->path); free(path->link); return 0; } ++item->file_count; return 1; } static const struct payload_path *find_path(const struct local_item *item, const char *path) { struct payload_path key = {(char *)path, NULL, 0}; return item->file_count ? bsearch(&key, item->file_paths, item->file_count, sizeof *item->file_paths, file_path_order) : NULL; } static int has_file(const struct local_item *item, const char *absolute) { return find_path(item, absolute + 1) != NULL; } static int command_target(const struct local_item *item, const char *path) { char *current = strdup(path); size_t hop; int result = 0; if (!current) return 0; for (hop = 0; hop < 16; ++hop) { const struct payload_path *entry = find_path(item, current); char *next; if (!entry) break; if (!entry->link) { result = (entry->mode & 0111) != 0; break; } next = holy_relative_link_path(current, strlen(current), entry->link, ""); if (!next) break; free(current); current = next; } free(current); return result; } static int has_command(const struct local_item *item, const char *name) { static const char *const dirs[] = {"usr/bin/", "bin/", "usr/sbin/", "sbin/"}; size_t i, length = strlen(name); for (i = 0; i < sizeof dirs / sizeof *dirs; ++i) { size_t prefix = strlen(dirs[i]); char *path = malloc(prefix + length + 1); int found; if (!path) return 0; memcpy(path, dirs[i], prefix); memcpy(path + prefix, name, length + 1); found = command_target(item, path); free(path); if (found) return 1; } return 0; } static int has_soname(const struct local_item *item, const char *name) { size_t i; for (i = 0; i < item->scan.count; ++i) { const struct holy_scanned_file *file = &item->scan.files[i]; if (file->elf.type == ET_DYN && !(file->elf.flags1 & DF_1_PIE) && file->elf.soname && !strcmp(file->elf.soname, name) && !strcmp(file->runtime, item->identity.libc)) return 1; } return 0; } static int package_requirements(struct local_item *local, struct holy_solver_item *items, size_t count) { size_t i, j, k; for (i = 0; i < count; ++i) for (j = 0; j < local[i].package_edge_count; ++j) { const struct package_edge *edge = &local[i].package_edges[j]; size_t index = edge->requirement; const char *family = local[i].identity.version_family; const char *relation = edge->relation; const struct version_adapter *adapter = version_adapter(family); int constrained = strcmp(relation, "any") != 0; char *capability; size_t length = strlen(local[i].requirement_ids[index]) + 80; if (constrained && !adapter) { fprintf(stderr, "holypkg: unsupported-version-family consumer=%s requirement=%s\n", local[i].identity.digest, local[i].requirement_ids[index]); return 0; } capability = malloc(length); if (!capability) return 0; snprintf(capability, length, "package-edge:%s:%s", local[i].identity.digest, local[i].requirement_ids[index]); if (j && local[i].package_edges[j - 1].requirement == index) free(capability); else { free((char *)local[i].requirements[index].first); local[i].requirements[index].first = capability; } capability = (char *)local[i].requirements[index].first; for (k = 0; k < count; ++k) { int matches = package_edge_matches(&local[i], edge, &local[k]); if (matches < 0 || (matches && !add_provide(&items[k], capability))) { return 0; } } } return 1; } static int provides(const struct holy_solver_item *item, const char *capability) { size_t i; for (i = 0; i < item->provides_count; ++i) if (!strcmp(item->provides[i], capability)) return 1; return 0; } static int compatible(const struct holy_scanned_file *a, const struct holy_scanned_file *b) { return a->elf.elf_class == b->elf.elf_class && a->elf.machine == b->elf.machine && !strcmp(a->runtime, b->runtime); } static int literal_path(const char *path) { const char *part, *end; if (path[0] != '/' || !path[1] || strchr(path, '$')) return 0; for (part = path + 1; ; part = end + 1) { size_t length; end = strchr(part, '/'); length = end ? (size_t)(end - part) : strlen(part); if (!length || (length == 1 && part[0] == '.') || (length == 2 && !memcmp(part, "..", 2))) return 0; if (!end) return 1; } } static int needed_matches(const struct holy_scanned_file *consumer, const struct holy_scanned_file *provider, const char *needed) { size_t i, j; if (!compatible(consumer, provider) || provider->elf.type != ET_DYN || (provider->elf.flags1 & DF_1_PIE)) return 0; if (needed[0] == '/') { if (!literal_path(needed) || strcmp(provider->path, needed + 1)) return 0; } else if (!provider->elf.soname || strcmp(provider->elf.soname, needed)) return 0; for (i = 0; i < consumer->elf.version_count; ++i) { const struct holy_elf_version *v = &consumer->elf.versions[i]; if (v->weak || strcmp(v->provider, needed)) continue; for (j = 0; j < provider->elf.defined_version_count; ++j) if (!strcmp(v->name, provider->elf.defined_versions[j].name)) break; if (j == provider->elf.defined_version_count) return 0; } for (i = 0; i < consumer->elf.symbol_count; ++i) { const struct holy_elf_symbol *s = &consumer->elf.symbols[i]; if (s->section || s->binding == STB_WEAK || !s->provider || strcmp(s->provider, needed)) continue; if (!holy_elf_exports_symbol(&provider->elf, s)) return 0; } return 1; } static int direct_provider(const struct holy_scanned_file *consumer, const struct holy_scanned_file *provider) { size_t i; if (consumer == provider) return 1; if (consumer->elf.interpreter && consumer->elf.interpreter[0] == '/' && !strcmp(consumer->elf.interpreter + 1, provider->path)) return 1; for (i = 0; i < consumer->elf.needed_count; ++i) { const char *needed = consumer->elf.needed[i]; if ((needed[0] == '/' && literal_path(needed) && !strcmp(needed + 1, provider->path)) || (provider->elf.soname && !strcmp(needed, provider->elf.soname))) return 1; } return 0; } static int elf_requirement(struct local_item *local, struct holy_solver_item *items, size_t count, size_t consumer_index, const struct holy_scanned_file *file, const char *kind, const char *target, const struct holy_elf_symbol *symbol) { struct local_item *consumer = &local[consumer_index]; EVP_MD_CTX *ctx = EVP_MD_CTX_new(); const char *parts[] = {consumer->identity.name, consumer->identity.arch, consumer->identity.libc, file->path, kind, target}; unsigned char hash[32]; unsigned int length; char id[69] = "elf-", capability[140]; struct elf_edge *edges; size_t i, j; int ok = 0; if (!ctx || EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) goto done; for (i = 0; i < sizeof parts / sizeof *parts; ++i) if (EVP_DigestUpdate(ctx, parts[i], strlen(parts[i]) + 1) != 1) goto done; if (EVP_DigestFinal_ex(ctx, hash, &length) != 1 || length != sizeof hash) goto done; for (i = 0; i < sizeof hash; ++i) snprintf(id + 4 + i * 2, 3, "%02x", hash[i]); snprintf(capability, sizeof capability, "elf:%s:%s", consumer->identity.digest, id); edges = realloc(consumer->edges, (consumer->edge_count + 1) * sizeof *edges); if (!edges) goto done; consumer->edges = edges; edges[consumer->edge_count].requirement = consumer->requirement_count; edges[consumer->edge_count].path = file->path; edges[consumer->edge_count].kind = kind; edges[consumer->edge_count].target = target; edges[consumer->edge_count].owned_target = NULL; edges[consumer->edge_count].file = file; edges[consumer->edge_count].symbol = symbol; if (!add_requirement(consumer, id, capability)) goto done; ++consumer->edge_count; for (i = 0; i < count; ++i) { for (j = 0; j < local[i].scan.count; ++j) { const struct holy_scanned_file *candidate = &local[i].scan.files[j]; int matches; if (!strcmp(kind, "soname") || !strcmp(kind, "needed-path")) matches = needed_matches(file, candidate, target); else if (!strcmp(kind, "interpreter")) matches = target[0] == '/' && !strcmp(target + 1, candidate->path) && (candidate->mode & 0111) && compatible(file, candidate); else matches = compatible(file, candidate) && direct_provider(file, candidate) && holy_elf_exports_symbol(&candidate->elf, symbol); if (matches && !add_provide(&items[i], capability)) goto done; } } ok = 1; done: EVP_MD_CTX_free(ctx); return ok; } static int script_path_edge(struct local_item *local, struct holy_solver_item *items, size_t count, size_t consumer_index, const struct holy_scanned_script *script, const char *kind, const char *path, const char *link_target) { struct local_item *consumer = &local[consumer_index]; const char *parts[] = {consumer->identity.name, consumer->identity.arch, consumer->identity.libc, script->path, kind, path}; EVP_MD_CTX *ctx = EVP_MD_CTX_new(); struct elf_edge *edges; unsigned char hash[32]; unsigned int length; char id[72] = "script-", capability[160]; size_t i, j; int ok = 0; if (!ctx || EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) goto done; for (i = 0; i < sizeof parts / sizeof *parts; ++i) if (EVP_DigestUpdate(ctx, parts[i], strlen(parts[i]) + 1) != 1) goto done; if (EVP_DigestFinal_ex(ctx, hash, &length) != 1 || length != sizeof hash) goto done; for (i = 0; i < sizeof hash; ++i) snprintf(id + 7 + i * 2, 3, "%02x", hash[i]); snprintf(capability, sizeof capability, "script:%s:%s", consumer->identity.digest, id); edges = realloc(consumer->edges, (consumer->edge_count + 1) * sizeof *edges); if (!edges) goto done; consumer->edges = edges; edges[consumer->edge_count].requirement = consumer->requirement_count; edges[consumer->edge_count].path = script->path; edges[consumer->edge_count].kind = kind; edges[consumer->edge_count].owned_target = malloc(strlen(path) + 2); if (!edges[consumer->edge_count].owned_target) goto done; sprintf(edges[consumer->edge_count].owned_target, "/%s", path); edges[consumer->edge_count].target = edges[consumer->edge_count].owned_target; edges[consumer->edge_count].file = NULL; edges[consumer->edge_count].symbol = NULL; if (!add_requirement(consumer, id, capability)) { free(edges[consumer->edge_count].owned_target); goto done; } ++consumer->edge_count; for (i = 0; i < count; ++i) { if (link_target) { for (j = 0; j < local[i].scan.symlink_count; ++j) { const struct holy_scanned_symlink *candidate = &local[i].scan.symlinks[j]; if (!strcmp(candidate->path, path) && !strcmp(candidate->target, link_target) && !add_provide(&items[i], capability)) goto done; } } else for (j = 0; j < local[i].scan.count; ++j) { const struct holy_scanned_file *candidate = &local[i].scan.files[j]; if (!strcmp(candidate->path, path) && (candidate->mode & 0111) && (candidate->elf.type == ET_EXEC || (candidate->elf.type == ET_DYN && ((candidate->elf.flags1 & DF_1_PIE) || candidate->elf.interpreter))) && !add_provide(&items[i], capability)) goto done; } } ok = 1; done: EVP_MD_CTX_free(ctx); return ok; } static int script_requirement(struct local_item *local, struct holy_solver_item *items, size_t count, size_t consumer_index, const struct holy_scanned_script *script) { char *path = strdup(script->interpreter + 1); char *visited[16] = {0}; size_t hop, i, j; int result = 0; if (!path) return 0; for (hop = 0; hop < 16; ++hop) { const char *target = NULL; size_t prefix = 0; char *next; for (i = 0; i < hop; ++i) if (!strcmp(path, visited[i])) { result = 3; goto done; } visited[hop] = strdup(path); if (!visited[hop]) goto done; for (i = 0; i < count; ++i) for (j = 0; j < local[i].scan.symlink_count; ++j) { const struct holy_scanned_symlink *candidate = &local[i].scan.symlinks[j]; size_t n = strlen(candidate->path); if (n <= prefix || strncmp(path, candidate->path, n) || (path[n] && path[n] != '/')) continue; prefix = n; target = candidate->target; } if (!target) { result = script_path_edge(local, items, count, consumer_index, script, "shebang", path, NULL); break; } for (i = 0; i < count; ++i) for (j = 0; j < local[i].scan.symlink_count; ++j) { const struct holy_scanned_symlink *candidate = &local[i].scan.symlinks[j]; if (strlen(candidate->path) == prefix && !strncmp(path, candidate->path, prefix) && strcmp(candidate->target, target)) { fprintf(stderr, "holypkg: ambiguous script alias %.*s\n", (int)prefix, path); result = 3; goto done; } } next = holy_relative_link_path(path, prefix, target, path + prefix); if (!next) { result = 3; goto done; } path[prefix] = 0; if (!script_path_edge(local, items, count, consumer_index, script, "path-alias", path, target)) { free(next); goto done; } free(path); path = next; } if (hop == 16) result = 3; done: for (i = 0; i < 16; ++i) free(visited[i]); free(path); return result; } static int elf_requirements(struct local_item *local, struct holy_solver_item *items, size_t count) { size_t i, j, k; for (i = 0; i < count; ++i) for (j = 0; j < local[i].scan.script_count; ++j) { int status = script_requirement(local, items, count, i, &local[i].scan.scripts[j]); if (status != 1) return status; } for (i = 0; i < count; ++i) for (j = 0; j < local[i].scan.count; ++j) { const struct holy_scanned_file *f = &local[i].scan.files[j]; if (f->elf.interpreter && !elf_requirement(local, items, count, i, f, "interpreter", f->elf.interpreter, NULL)) return 0; for (k = 0; k < f->elf.needed_count; ++k) { if (strchr(f->elf.needed[k], '/') && !literal_path(f->elf.needed[k])) { fputs("holypkg: DT_NEEDED paths require a launch context\n", stderr); return 0; } if (!elf_requirement(local, items, count, i, f, f->elf.needed[k][0] == '/' ? "needed-path" : "soname", f->elf.needed[k], NULL)) return 0; } for (k = 0; k < f->elf.symbol_count; ++k) { const struct holy_elf_symbol *s = &f->elf.symbols[k]; if (s->section || !s->name[0] || s->binding == STB_WEAK || s->binding == STB_LOCAL) continue; if (s->provider) { size_t n; for (n = 0; n < f->elf.needed_count; ++n) if (!strcmp(f->elf.needed[n], s->provider)) break; if (n == f->elf.needed_count) return 0; } else if (!elf_requirement(local, items, count, i, f, "symbol", s->name, s)) return 0; } } return 1; } static void json_string(const char *s) { const unsigned char *p = (const unsigned char *)s; putchar('"'); for (; *p; ++p) { if (*p == '"' || *p == '\\') { putchar('\\'); putchar(*p); } else if (*p < 32 || *p >= 127) printf("\\u%04x", *p); else putchar(*p); } putchar('"'); } static int symbol_context(const struct local_item *consumer, const struct elf_edge *edge, const struct local_item *provider, const struct holy_solver_item *item) { size_t i, j; for (i = 0; i < provider->scan.count; ++i) { const struct holy_scanned_file *f = &provider->scan.files[i]; if (!compatible(edge->file, f) || !holy_elf_exports_symbol(&f->elf, edge->symbol)) continue; if (f == edge->file) return 1; for (j = 0; j < consumer->edge_count; ++j) { const struct elf_edge *dependency = &consumer->edges[j]; if (dependency->file != edge->file) continue; if ((!strcmp(dependency->kind, "soname") && f->elf.soname && !strcmp(dependency->target, f->elf.soname)) || ((!strcmp(dependency->kind, "interpreter") || !strcmp(dependency->kind, "needed-path")) && dependency->target[0] == '/' && !strcmp(dependency->target + 1, f->path))) if (provides(item, consumer->requirements[dependency->requirement].first)) return 1; } } return 0; } static void report_edges(const struct local_item *local, const struct holy_solver_item *items, size_t count, const int *selected, int json) { size_t i, j, k; for (i = 0; i < count; ++i) { if (selected ? !selected[i] : i != 0) continue; for (j = 0; j < local[i].edge_count; ++j) { const struct elf_edge *edge = &local[i].edges[j]; const char *id = local[i].requirement_ids[edge->requirement]; const char *cap = local[i].requirements[edge->requirement].first; int first = 1; if (json) { printf("{\"schema\":\"holy-local-solve-1\",\"type\":\"%s\",\"id\":\"%s\",\"consumer\":\"%s\",\"path\":", !strcmp(edge->kind, "shebang") ? "script-edge" : "elf-edge", id, local[i].identity.digest); json_string(edge->path); printf(",\"kind\":\"%s\",\"target\":", edge->kind); json_string(edge->target); fputs(",\"providers\":[", stdout); } else { printf("%s %s consumer=%s path=", !strcmp(edge->kind, "shebang") ? "script-edge" : "elf-edge", id, local[i].identity.digest); json_string(edge->path); printf(" kind=%s target=", edge->kind); json_string(edge->target); fputs(" providers=", stdout); } for (k = 0; k < count; ++k) { if ((selected && !selected[k]) || !provides(&items[k], cap)) continue; if (!first) putchar(','); if (json) json_string(local[k].identity.digest); else fputs(local[k].identity.digest, stdout); first = 0; } puts(json ? "]}" : ""); } } } static int supported_metadata(const char *snapshot) { struct archive *a = archive_read_new(); struct archive_entry *entry; int status, hooks = 0, transform = 0, ok = 0; if (!a || archive_read_support_filter_lz4(a) != ARCHIVE_OK || archive_read_support_format_tar(a) != ARCHIVE_OK || archive_read_open_filename(a, snapshot, 8192) != ARCHIVE_OK) goto done; while ((status = archive_read_next_header(a, &entry)) == ARCHIVE_OK) { const char *name = archive_entry_pathname(entry); if (!strcmp(name, "HOLY/hooks")) { hooks = 1; if (archive_entry_filetype(entry) != AE_IFREG || archive_entry_size(entry) < 0 || archive_entry_size(entry) > 1024 * 1024) goto done; } if (!strcmp(name, "HOLY/transform")) { transform = 1; if (archive_entry_filetype(entry) != AE_IFREG) goto done; } if (archive_read_data_skip(a) != ARCHIVE_OK) goto done; } ok = status == ARCHIVE_EOF && hooks && transform; done: if (a) archive_read_free(a); return ok; } static const char *missing_requirement(const struct local_item *local, const struct holy_solver_item *items, size_t count, const char *const *skip_ids, size_t skip_count, size_t *consumer_index, size_t *requirement_index) { unsigned char *seen = calloc(count, 1); size_t *queue = malloc(count * sizeof *queue); size_t head = 0, tail = 1, j; const char *missing = NULL; if (!seen || !queue) goto done; seen[0] = 1; queue[0] = 0; while (head < tail) { size_t current = queue[head++]; const struct local_item *consumer = &local[current]; for (j = 0; j < consumer->requirement_count; ++j) { size_t i, matches = 0, provider = 0; for (i = 0; i < count; ++i) if (provides(&items[i], consumer->requirements[j].first)) { ++matches; provider = i; } if (!matches) { size_t skip; for (skip = 0; skip < skip_count; ++skip) if (!strncmp(skip_ids[skip], consumer->identity.digest, 64) && skip_ids[skip][64] == ':' && !strcmp(skip_ids[skip] + 65, consumer->requirement_ids[j])) break; if (skip < skip_count) continue; missing = consumer->requirement_ids[j]; *consumer_index = current; *requirement_index = j; goto done; } if (matches == 1 && !seen[provider]) { seen[provider] = 1; queue[tail++] = provider; } } } done: free(seen); free(queue); return missing; } static int capture_missing(const struct local_item *consumer, size_t requirement, struct holy_missing_requirement *missing) { size_t k; const char *id = consumer->requirement_ids[requirement]; const char *kind = NULL, *name = NULL, *path = NULL, *original; for (k = 0; k < consumer->edge_count; ++k) { const struct elf_edge *edge = &consumer->edges[k]; if (edge->requirement != requirement) continue; kind = !strcmp(edge->kind, "soname") ? "soname" : !strcmp(edge->kind, "symbol") ? NULL : "file"; name = edge->target; if (!strcmp(edge->kind, "soname")) path = edge->path; break; } if (!name) { original = consumer->original_requirements[requirement]; if (!strncmp(original, "package-or:", 11)) { kind = "package-or"; name = original + 11; } } if (!name) { for (k = 0; k < consumer->package_edge_count; ++k) if (consumer->package_edges[k].requirement == requirement) { kind = consumer->package_edges[k].kind; name = consumer->package_edges[k].name; break; } } if (!name) { original = consumer->original_requirements[requirement]; if (!strncmp(original, "package:", 8)) { kind = "package"; name = original + 8; } else if (!strncmp(original, "file:", 5)) { kind = "file"; name = original + 5; } else if (!strncmp(original, "command:", 8)) { kind = "command"; name = original + 8; } else if (!strncmp(original, "soname:", 7)) { kind = "soname"; name = original + 7; } } if (!kind || !name || !*name) return 0; missing->consumer = strdup(consumer->identity.digest); missing->id = strdup(id); missing->kind = strdup(kind); missing->name = strdup(name); if (path) missing->path = strdup(path); if (missing->consumer && missing->id && missing->kind && missing->name && (!path || missing->path)) return 1; holy_missing_requirement_free(missing); return 0; } static char *choice_requirement(const char *choice, const char **digest) { const char *equal = strchr(choice, '='); char *id; size_t i, length; if (!equal || equal == choice || strlen(equal + 1) != 64) return NULL; length = (size_t)(equal - choice); if (length > 65536) return NULL; for (i = 0; i < length; ++i) if (!((choice[i] >= 'a' && choice[i] <= 'z') || (choice[i] >= 'A' && choice[i] <= 'Z') || (choice[i] >= '0' && choice[i] <= '9') || choice[i] == '-' || choice[i] == '_' || choice[i] == '.')) return NULL; for (i = 0; i < 64; ++i) if (!((equal[i + 1] >= '0' && equal[i + 1] <= '9') || (equal[i + 1] >= 'a' && equal[i + 1] <= 'f'))) return NULL; id = strndup(choice, length); if (id) *digest = equal + 1; return id; } static int artifact_order(const void *a, const void *b) { return strcmp(*(const char *const *)a, *(const char *const *)b); } static int edge_order(const void *a, const void *b) { const struct holy_resolved_edge *x = a, *y = b; int c = strcmp(x->consumer, y->consumer); return c ? c : strcmp(x->id, y->id); } static int collect_result(const struct local_item *local, const struct holy_solver_item *items, size_t count, const int *selected, struct holy_resolution *out) { size_t i, j, k; memcpy(out->root, local[0].identity.digest, sizeof out->root); out->artifacts = calloc(count, sizeof *out->artifacts); if (!out->artifacts) return 0; for (i = 0; i < count; ++i) if (selected[i]) { out->artifacts[out->artifact_count] = strdup(local[i].identity.digest); if (!out->artifacts[out->artifact_count]) return 0; ++out->artifact_count; for (j = 0; j < local[i].requirement_count; ++j) { struct holy_resolved_edge *next, *edge; const char *path = "-", *kind = "package", *target = NULL; size_t provider = count; for (k = 0; k < count; ++k) if (selected[k] && provides(&items[k], local[i].requirements[j].first)) { provider = k; break; } if (provider == count) return 0; for (k = 0; k < local[i].edge_count; ++k) { const struct elf_edge *e = &local[i].edges[k]; if (e->requirement == j) { path = e->path; kind = e->kind; target = e->target; break; } } if (!target) { const char *original = local[i].original_requirements[j]; if (!strncmp(original, "package-or:", 11)) { for (k = 0; k < local[i].package_edge_count; ++k) if (local[i].package_edges[k].requirement == j && package_edge_matches(&local[i], &local[i].package_edges[k], &local[provider]) == 1) { target = local[i].package_edges[k].name; break; } if (!target) return 0; } else if (!strncmp(original, "package:", 8)) target = original + 8; else if (!strncmp(original, "file:", 5)) { kind = "file"; target = original + 5; } else if (!strncmp(original, "command:", 8)) { kind = "command"; target = original + 8; } else if (!strncmp(original, "soname:", 7)) { kind = "soname"; target = original + 7; } else return 0; } if (out->edge_count >= 65536) return 0; next = realloc(out->edges, (out->edge_count + 1) * sizeof *next); if (!next) return 0; out->edges = next; edge = &next[out->edge_count++]; memset(edge, 0, sizeof *edge); edge->consumer = strdup(local[i].identity.digest); edge->id = strdup(local[i].requirement_ids[j]); edge->path = strdup(path); edge->kind = strdup(kind); edge->target = strdup(target); edge->provider = strdup(local[provider].identity.digest); if (!edge->consumer || !edge->id || !edge->path || !edge->kind || !edge->target || !edge->provider) return 0; } } qsort(out->artifacts, out->artifact_count, sizeof *out->artifacts, artifact_order); if (out->edge_count) qsort(out->edges, out->edge_count, sizeof *out->edges, edge_order); return 1; } static int resolve(const char *const *paths, size_t count, int json, const char *generation, const char *choice, struct holy_resolution *output, int all, struct holy_missing_requirement *missing_output, const char *const *skip_ids, size_t skip_count) { struct local_item *local = NULL; struct holy_solver_item *items = NULL; int *selected = NULL, result = 6; size_t i, j, prepared = 0; int solved; const char *unresolved = NULL; size_t missing_consumer = 0, missing_index = 0; const char *unknown_context = NULL; const char *chosen_digest = NULL; char *chosen_id = NULL, *choice_capability = NULL; if (choice) { chosen_id = choice_requirement(choice, &chosen_digest); if (!chosen_id) { result = 2; goto done; } } if (!paths || !count || count > 10000) goto done; local = calloc(count, sizeof *local); items = calloc(count, sizeof *items); selected = calloc(count, sizeof *selected); if (!local || !items || !selected) goto done; for (i = 0; i < count; ++i) { char *snapshot; if (!paths[i]) goto done; snapshot = holy_stage_local(paths[i], "holy-resolve"); if (!snapshot) goto done; prepared = i + 1; if (!holy_verify_visit(snapshot, collect_file_path, &local[i]) || !holy_scan_collect(snapshot, &local[i].scan) || !holy_package_identity(snapshot, &local[i].identity) || strcmp(local[i].identity.os, "linux") || !supported_metadata(snapshot)) { unlink(snapshot); free(snapshot); goto done; } if (!holy_provides_visit(snapshot, package_claim, &local[i])) { unlink(snapshot); free(snapshot); goto done; } local[i].capability = package_capability(local[i].identity.name); if (!local[i].capability || !holy_deps_visit(snapshot, exact_requirement, &local[i])) { unlink(snapshot); free(snapshot); goto done; } for (j = 0; j < i; ++j) if (!strcmp(local[j].identity.digest, local[i].identity.digest)) { unlink(snapshot); free(snapshot); goto done; } items[i].id = local[i].identity.digest; if (!add_provide(&items[i], local[i].capability)) { unlink(snapshot); free(snapshot); goto done; } for (j = 0; j < local[i].claim_count; ++j) if (!add_provide(&items[i], local[i].claims[j].capability)) { unlink(snapshot); free(snapshot); goto done; } if (local[i].file_count) qsort(local[i].file_paths, local[i].file_count, sizeof *local[i].file_paths, file_path_order); unlink(snapshot); free(snapshot); } if (!package_requirements(local, items, count)) goto done; for (i = 0; i < count; ++i) for (j = 0; j < local[i].scan.script_count; ++j) { const struct holy_scanned_script *script = &local[i].scan.scripts[j]; if (script->kind != 1 || !literal_path(script->interpreter)) { fprintf(stderr, "holypkg: script-interpreter-decision consumer=%s interpreter=%s\n", script->path, script->interpreter); result = 3; goto done; } } { int status = elf_requirements(local, items, count); if (status != 1) { if (status == 3) result = 3; goto done; } } for (i = 0; i < count; ++i) { items[i].requires = local[i].requirements; items[i].requires_count = local[i].requirement_count; } if (chosen_id) { size_t requirement = local[0].requirement_count, provider = count; char *replacement; for (j = 0; j < local[0].requirement_count; ++j) if (!strcmp(local[0].requirement_ids[j], chosen_id)) { requirement = j; break; } for (i = 0; i < count; ++i) if (!strcmp(local[i].identity.digest, chosen_digest)) { provider = i; break; } if (requirement == local[0].requirement_count || provider == count || !provides(&items[provider], local[0].requirements[requirement].first)) { result = 3; goto done; } choice_capability = malloc(strlen(chosen_id) + 8); if (!choice_capability) goto done; sprintf(choice_capability, "choice:%s", chosen_id); replacement = strdup(choice_capability); if (!replacement) goto done; if (!add_provide(&items[provider], choice_capability)) { free(replacement); goto done; } free((char *)local[0].requirements[requirement].first); local[0].requirements[requirement].first = replacement; } solved = all ? holy_solve_exact_set(items, count, selected) : holy_solve_exact_unique(items, count, items[0].id, selected); if (solved == 1) for (i = 0; i < count; ++i) if (selected[i]) { if (local[i].unsupported_id) { unresolved = local[i].unsupported_id; unknown_context = "unsupported-requirement"; result = 3; goto done; } for (j = 0; j < local[i].requirement_count; ++j) { size_t k, matches = 0; for (k = 0; k < count; ++k) if (selected[k] && provides(&items[k], local[i].requirements[j].first)) ++matches; if (matches > 1) { result = 3; goto done; } } for (j = 0; j < local[i].edge_count; ++j) { const struct elf_edge *edge = &local[i].edges[j]; size_t k; if (!edge->symbol) continue; for (k = 0; k < count; ++k) if (selected[k] && provides(&items[k], local[i].requirements[edge->requirement].first) && !symbol_context(&local[i], edge, &local[k], &items[k])) { result = 3; goto done; } } } if (solved == 1) { size_t selected_count = 0; if (output && !collect_result(local, items, count, selected, output)) { result = 1; goto done; } if (json >= 0) report_edges(local, items, count, selected, json); for (i = 0; i < count; ++i) if (selected[i]) { if (json > 0) printf("{\"schema\":\"holy-local-solve-1\",\"type\":\"selected\",\"sha256\":\"%s\"}\n", local[i].identity.digest); else if (!json) printf("selected %s\n", local[i].identity.digest); ++selected_count; } if (json > 0) { printf("{\"schema\":\"holy-local-solve-1\",\"type\":\"summary\",\"count\":%zu", selected_count); if (generation) printf(",\"generation\":\"%s\"", generation); puts("}"); } else if (!json && generation) printf("generation %s\n", generation); result = 0; } else if (solved == 3) result = 3; else if (solved == 2) { result = 4; unresolved = missing_requirement(local, items, count, skip_ids, skip_count, &missing_consumer, &missing_index); if (missing_output && unresolved && !capture_missing(&local[missing_consumer], missing_index, missing_output)) result = 3; if (unresolved) for (j = 0; j < local[missing_consumer].edge_count; ++j) { const struct elf_edge *edge = &local[missing_consumer].edges[j]; if (edge->requirement != missing_index) continue; if (edge->symbol) unknown_context = "unknown-symbol-scope"; else if (!strcmp(edge->kind, "interpreter")) unknown_context = "unknown-interpreter-context"; if (unknown_context) result = 3; } } done: if (result && output) holy_resolution_free(output); if (json >= 0 && (result == 3 || result == 4) && prepared == count && local && items) report_edges(local, items, count, NULL, json); if (result && !missing_output) fprintf(stderr, "holypkg: local resolution %s\n", result == 2 ? "has an invalid choice" : result == 3 ? "needs provider choice" : result == 4 ? "has a dependency conflict" : "requires unsupported data or failed"); if (unresolved && !missing_output) fprintf(stderr, "holypkg: unresolved requirement %s\n", unresolved); if (result && json > 0) { if (unresolved) printf("{\"schema\":\"holy-local-solve-1\",\"type\":\"error\",\"code\":\"%s\",\"requirement\":\"%s\"}\n", unknown_context ? unknown_context : "dependency-conflict", unresolved); else printf("{\"schema\":\"holy-local-solve-1\",\"type\":\"error\",\"code\":\"%s\"}\n", result == 2 ? "invalid-query" : result == 3 ? "decision-required" : result == 4 ? "dependency-conflict" : "unsupported-input"); } for (i = 0; i < prepared; ++i) { for (j = 0; j < local[i].requirement_count; ++j) free((char *)local[i].requirements[j].first); for (j = 0; j < local[i].requirement_count; ++j) free(local[i].requirement_ids[j]); for (j = 0; j < local[i].requirement_count; ++j) free(local[i].original_requirements[j]); free(local[i].requirements); free(local[i].requirement_ids); free(local[i].original_requirements); free(local[i].capability); free(local[i].unsupported_id); for (j = 0; j < local[i].edge_count; ++j) free(local[i].edges[j].owned_target); free(local[i].edges); for (j = 0; j < local[i].package_edge_count; ++j) { free(local[i].package_edges[j].kind); free(local[i].package_edges[j].name); free(local[i].package_edges[j].arch); free(local[i].package_edges[j].libc); free(local[i].package_edges[j].relation); free(local[i].package_edges[j].version); } free(local[i].package_edges); for (j = 0; j < local[i].claim_count; ++j) { free(local[i].claims[j].capability); free(local[i].claims[j].version); } free(local[i].claims); for (j = 0; j < local[i].file_count; ++j) { free(local[i].file_paths[j].path); free(local[i].file_paths[j].link); } free(local[i].file_paths); holy_scan_free(&local[i].scan); holy_package_identity_free(&local[i].identity); } if (items) for (i = 0; i < count; ++i) { for (j = 0; j < items[i].provides_count; ++j) free((void *)items[i].provides[j]); free((void *)items[i].provides); } free(items); free(local); free(selected); free(chosen_id); free(choice_capability); return result; } int holy_resolve_local(const char *const *paths, size_t count, int json, const char *generation, const char *choice) { return resolve(paths, count, json, generation, choice, NULL, 0, NULL, NULL, 0); } int holy_resolve_collect(const char *const *paths, size_t count, const char *choice, struct holy_resolution *result) { memset(result, 0, sizeof *result); return resolve(paths, count, -1, NULL, choice, result, 0, NULL, NULL, 0); } int holy_resolve_collect_set(const char *const *paths, size_t count, struct holy_resolution *result) { memset(result, 0, sizeof *result); return resolve(paths, count, -1, NULL, NULL, result, 1, NULL, NULL, 0); } void holy_missing_requirement_free(struct holy_missing_requirement *missing) { free(missing->consumer); free(missing->id); free(missing->kind); free(missing->name); free(missing->path); memset(missing, 0, sizeof *missing); } int holy_resolve_missing(const char *const *paths, size_t count, const char *const *skip_ids, size_t skip_count, struct holy_missing_requirement *missing) { memset(missing, 0, sizeof *missing); return resolve(paths, count, -1, NULL, NULL, NULL, 0, missing, skip_ids, skip_count); } void holy_resolution_free(struct holy_resolution *result) { size_t i; for (i = 0; i < result->artifact_count; ++i) free(result->artifacts[i]); for (i = 0; i < result->edge_count; ++i) { struct holy_resolved_edge *e = &result->edges[i]; free(e->consumer); free(e->id); free(e->provider); free(e->path); free(e->kind); free(e->target); } free(result->artifacts); free(result->edges); memset(result, 0, sizeof *result); } static int record_token(FILE *stream, const char *value) { const unsigned char *p = (const unsigned char *)value; size_t encoded = 2; long offset = ftell(stream); if (!value || offset < 0 || offset > 16 * 1024 * 1024 - 2) return 0; for (; *p; ++p) { encoded += (*p < 32 || *p >= 127) ? 4 : (*p == '"' || *p == '\\') ? 2 : 1; if (encoded > 16 * 1024 * 1024 - (size_t)offset) return 0; } if (fputc('"', stream) == EOF) return 0; for (p = (const unsigned char *)value; *p; ++p) { if (*p == '"' || *p == '\\') { if (fputc('\\', stream) == EOF || fputc(*p, stream) == EOF) return 0; } else if (*p < 32 || *p >= 127) { if (fprintf(stream, "\\x%02x", *p) < 0) return 0; } else if (fputc(*p, stream) == EOF) return 0; } return fputc('"', stream) != EOF; } int holy_resolution_record(const struct holy_resolution *result, char **record, size_t *length) { FILE *stream; size_t i, j; int ok = 0; *record = NULL; *length = 0; if (!result->artifact_count || strlen(result->root) != 64) return 0; stream = open_memstream(record, length); if (!stream) return 0; if (fprintf(stream, "format holy-resolution-1\nscope artifact-candidates\nroot %s\n", result->root) < 0) goto done; for (i = 0; i < result->artifact_count; ++i) if (fprintf(stream, "artifact %s\n", result->artifacts[i]) < 0) goto done; for (i = 0; i < result->edge_count; ++i) { const struct holy_resolved_edge *e = &result->edges[i]; const char *fields[] = {e->consumer, e->id, e->provider, e->path, e->kind, e->target}; if (fputs("edge", stream) == EOF) goto done; for (j = 0; j < sizeof fields / sizeof *fields; ++j) if (fputc(' ', stream) == EOF || !record_token(stream, fields[j])) goto done; if (fputc('\n', stream) == EOF) goto done; } ok = 1; done: if (fclose(stream)) ok = 0; if (*length > 16 * 1024 * 1024) ok = 0; if (!ok) { free(*record); *record = NULL; *length = 0; } return ok; }