#define _POSIX_C_SOURCE 200809L #define _XOPEN_SOURCE 700 #include #include #include #include #include #include #include #include #include #include #include "common.h" #include "pkg.h" /* "MAJ.MIN.PAT[-TAG]". Each number is read with strtol and clamped to * 0..INT_MAX (a manifest is untrusted: sscanf's %d overflowing is * undefined); anything missing or unparseable is 0. */ semver_t gl_parse_semver(const char *str) { semver_t v = {0, 0, 0, NULL}; if (!str) { return v; } char buffer[64]; snprintf(buffer, sizeof(buffer), "%s", str); char *dash = strchr(buffer, '-'); if (dash) { *dash = '\0'; v.tag = strdup(dash + 1); } int *parts[] = { &v.maj, &v.min, &v.pat }; const char *p = buffer; for (int i = 0; i < 3 && *p; i++) { char *end; errno = 0; long n = strtol(p, &end, 10); if (end == p) { break; } *parts[i] = (errno || n < 0) ? 0 : n > INT_MAX ? INT_MAX : (int)n; if (*end != '.') { break; } p = end + 1; } return v; } void gl_free_semver(semver_t *v) { if (v && v->tag) { free (v->tag); }; } /* * gl_valid_pkg_ident * DESCRIPTION: checks that a package name or repo name is safe to use as a * single path component: non-empty, only [A-Za-z0-9._-], and not * "." or "..". Both come straight out of an untrusted manifest (or * off a command line) and get spliced into store paths, so * anything else could escape ctx->store_path. * PARAMETERS: * ~ const char *s (the identifier to check; NULL is invalid) * RETURNS: * ~ true if safe, false otherwise */ bool gl_valid_pkg_ident(const char *s) { if (!s || s[0] == '\0') { return false; } if (strcmp(s, ".") == 0 || strcmp(s, "..") == 0) { return false; } for (const char *p = s; *p; p++) { char c = *p; if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-')) { return false; } } return true; } /* A manifest comes out of an untrusted archive. libconfig honours * `@include "FILE"` at the start of a line, which would have it read any * file (as root, under syspkg): such a manifest is refused. */ static char * read_manifest_text(const char *filename) { FILE *f = fopen(filename, "rb"); if (!f) { return NULL; } enum { MAX_MANIFEST = 1024 * 1024 }; char *buf = malloc(MAX_MANIFEST + 1); size_t n = buf ? fread(buf, 1, MAX_MANIFEST + 1, f) : 0; bool bad = !buf || ferror(f) || n > MAX_MANIFEST || memchr(buf, '\0', n); fclose(f); if (bad) { free(buf); return NULL; } buf[n] = '\0'; for (const char *line = buf; line; line = strchr(line, '\n')) { if (*line == '\n') { line++; } while (*line == ' ' || *line == '\t') { line++; } if (strncmp(line, "@include", 8) == 0) { free(buf); return NULL; } } return buf; } /* PKG_DEPS / PKG_CONFS: a list of { PKG_NAME; PKG_VER; }, each name a * gl_valid_pkg_ident, into a NULL-name-terminated array of * (name, version) pairs, names and vers parallel. false if malformed. */ static bool manifest_relations(config_t *cfg, const char *key, char ***names, semver_t **vers) { *names = NULL; *vers = NULL; config_setting_t *l = config_lookup(cfg, key); if (!l) { return true; } if (!config_setting_is_list(l)) { return false; } int count = config_setting_length(l); *names = calloc((size_t)count + 1, sizeof(**names)); *vers = calloc((size_t)count + 1, sizeof(**vers)); if (!*names || !*vers) { return false; } for (int i = 0; i < count; i++) { config_setting_t *d = config_setting_get_elem(l, (unsigned)i); const char *name = NULL, *ver = NULL; config_setting_lookup_string(d, "PKG_NAME", &name); config_setting_lookup_string(d, "PKG_VER", &ver); if (!gl_valid_pkg_ident(name) || !((*names)[i] = strdup(name))) { return false; } (*vers)[i] = gl_parse_semver(ver); } return true; } static void free_relations(char **names, semver_t *vers) { for (size_t i = 0; names && names[i]; i++) { free(names[i]); gl_free_semver(&vers[i]); } free(names); free(vers); } int gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg) { char *text = read_manifest_text(filename); if (!text) { return ENOENT; } config_t cfg; config_init(&cfg); int parsed = config_read_string(&cfg, text); free(text); if (!parsed) { config_destroy(&cfg); return ENOENT; } /* Validate before anything is strdup'd, so a rejected manifest leaves * pkg untouched and the caller has nothing to free. */ const char *tmp_str, *name = NULL, *repo = NULL; char **dnames, **cnames; semver_t *dvers, *cvers; config_lookup_string(&cfg, "PKG_NAME", &name); config_lookup_string(&cfg, "PKG_REPO", &repo); bool ok = gl_valid_pkg_ident(name) && gl_valid_pkg_ident(repo); bool rels = manifest_relations(&cfg, "PKG_DEPS", &dnames, &dvers); rels = manifest_relations(&cfg, "PKG_CONFS", &cnames, &cvers) && rels; if (!ok || !rels) { free_relations(dnames, dvers); free_relations(cnames, cvers); config_destroy(&cfg); return EINVAL; } pkg->pkg_name = strdup(name); pkg->pkg_repo = strdup(repo); if (config_lookup_string(&cfg, "PKG_DESC", &tmp_str)) { pkg->pkg_desc = strdup(tmp_str); } if (config_lookup_string(&cfg, "PKG_MAINT", &tmp_str)) { pkg->pkg_maint = strdup(tmp_str); } if (config_lookup_string(&cfg, "PKG_COPYRIGHT", &tmp_str)) { pkg->pkg_copyright = strdup(tmp_str); } if (config_lookup_string(&cfg, "PKG_VER", &tmp_str)) { pkg->pkg_ver = gl_parse_semver(tmp_str); } if (dnames) { size_t n = 0; while (dnames[n]) { n++; } pkg->pkg_dependencies = calloc(n + 1, sizeof(dep_t)); for (size_t i = 0; pkg->pkg_dependencies && i < n; i++) { pkg->pkg_dependencies[i].dep_name = dnames[i]; pkg->pkg_dependencies[i].dep_ver = dvers[i]; dnames[i] = NULL; } } if (cnames) { size_t n = 0; while (cnames[n]) { n++; } pkg->pkg_conflicts = calloc(n + 1, sizeof(conf_t)); for (size_t i = 0; pkg->pkg_conflicts && i < n; i++) { pkg->pkg_conflicts[i].conf_name = cnames[i]; pkg->pkg_conflicts[i].conf_ver = cvers[i]; cnames[i] = NULL; } } free_relations(dnames, dvers); free_relations(cnames, cvers); config_destroy(&cfg); if (!pkg->pkg_name || !pkg->pkg_repo || (dnames && !pkg->pkg_dependencies) || (cnames && !pkg->pkg_conflicts)) { gl_free_pkg(pkg); memset(pkg, 0, sizeof(*pkg)); return ENOMEM; } return 0; } void gl_free_pkg(gl_pkg_t *pkg) { if (!pkg) return; free(pkg->pkg_name); free(pkg->pkg_desc); free(pkg->pkg_maint); free(pkg->pkg_copyright); free(pkg->pkg_repo); gl_free_semver(&pkg->pkg_ver); if (pkg->pkg_dependencies) { for (dep_t *d = pkg->pkg_dependencies; d->dep_name; d++) { free(d->dep_name); gl_free_semver(&d->dep_ver); } free(pkg->pkg_dependencies); } if (pkg->pkg_conflicts) { for (conf_t *c = pkg->pkg_conflicts; c->conf_name; c++) { free(c->conf_name); gl_free_semver(&c->conf_ver); } free(pkg->pkg_conflicts); } } /* * Read integer `key` from a libconfig file (glacier.conf). Distinguishes * the failures a caller has to report differently: * GL_CONF_OK *out set * GL_CONF_ERR_READ file missing, unreadable, or not valid libconfig * GL_CONF_ERR_MISSING file fine, key absent * GL_CONF_ERR_TYPE key present but not an integer * There are deliberately no defaults here: a setting the caller needs and * the file doesn't have is an error, not a silent fallback (CLAUDE.md bug * 10 is what a plausible default hides). */ gl_conf_status_t gl_conf_get_int(const char *path, const char *key, int *out) { config_t cfg; config_init(&cfg); if (!config_read_file(&cfg, path)) { config_destroy(&cfg); return GL_CONF_ERR_READ; } config_setting_t *setting = config_lookup(&cfg, key); if (!setting) { config_destroy(&cfg); return GL_CONF_ERR_MISSING; } if (config_setting_type(setting) != CONFIG_TYPE_INT) { config_destroy(&cfg); return GL_CONF_ERR_TYPE; } *out = config_setting_get_int(setting); config_destroy(&cfg); return GL_CONF_OK; } /* A repos.conf url: https:// or file://, then at least one printable, * non-space ASCII character. http:// and everything else are refused. */ static bool valid_repo_url(const char *u) { const char *rest; if (strncmp(u, "https://", 8) == 0) rest = u + 8; else if (strncmp(u, "file://", 7) == 0) rest = u + 7; else return false; if (*rest == '\0') return false; for (; *rest; rest++) if (*rest < 0x21 || *rest > 0x7e) return false; return true; } /* * Read and check a whole repos.conf (format: gl_conf_repo_keys). On * GL_CONF_OK, *cfg is open (the caller config_destroy()s it) and *repos * is its `repos` list; on anything else nothing is left open. */ static gl_conf_status_t read_repos_conf(const char *path, config_t *cfg, config_setting_t **repos) { config_init(cfg); if (!config_read_file(cfg, path)) { config_destroy(cfg); return GL_CONF_ERR_READ; } config_setting_t *root = config_root_setting(cfg); config_setting_t *list = config_setting_get_member(root, "repos"); if (config_setting_length(root) != 1 || !list || config_setting_type(list) != CONFIG_TYPE_LIST) goto invalid; int n = config_setting_length(list); for (int i = 0; i < n; i++) { config_setting_t *e = config_setting_get_elem(list, (unsigned)i); if (config_setting_type(e) != CONFIG_TYPE_GROUP) goto invalid; config_setting_t *name = config_setting_get_member(e, "name"); config_setting_t *url = config_setting_get_member(e, "url"); config_setting_t *keys = config_setting_get_member(e, "keys"); if (!name || !keys || config_setting_length(e) != (url ? 3 : 2)) goto invalid; const char *nm = config_setting_get_string(name); if (!nm || !gl_valid_pkg_ident(nm)) goto invalid; if (url) { const char *u = config_setting_get_string(url); if (!u || !valid_repo_url(u)) goto invalid; } for (int j = 0; j < i; j++) { config_setting_t *prev = config_setting_get_member( config_setting_get_elem(list, (unsigned)j), "name"); if (strcmp(config_setting_get_string(prev), nm) == 0) goto invalid; } if (config_setting_type(keys) != CONFIG_TYPE_ARRAY && config_setting_type(keys) != CONFIG_TYPE_LIST) goto invalid; int nk = config_setting_length(keys); for (int k = 0; k < nk; k++) { const char *key = config_setting_get_string( config_setting_get_elem(keys, (unsigned)k)); if (!key || !gl_valid_pkg_ident(key)) goto invalid; } } *repos = list; return GL_CONF_OK; invalid: config_destroy(cfg); return GL_CONF_ERR_INVALID; } /* * The names of the keys repos.conf trusts for `repo` (REPO_DESIGN.txt * section 3), '\n'-separated (each followed by '\n') into out. * GL_CONF_OK out set (possibly "": the repo lists no keys) * GL_CONF_ERR_READ file missing, unreadable, or not valid libconfig * GL_CONF_ERR_MISSING the file is valid, but doesn't list `repo` * GL_CONF_ERR_INVALID anything else than the format below * GL_CONF_ERR_SPACE the names don't fit in outlen * The WHOLE file is checked on every call, not only `repo`'s entry, and * nothing unknown is ignored (a setting that silently never parses is * CLAUDE.md bug 10's class): * repos = ( { name = "base"; url = "https://..."; keys = [ "k" ]; }, ... ); * `repos` is the only top-level setting; each entry is a group with a * name (a gl_valid_pkg_ident, unique), an optional url (valid_repo_url) * and keys (an array or list of gl_valid_pkg_ident strings). */ gl_conf_status_t gl_conf_repo_keys(const char *path, const char *repo, char *out, size_t outlen) { if (!out || outlen == 0) return GL_CONF_ERR_SPACE; out[0] = '\0'; config_t cfg; config_setting_t *repos; gl_conf_status_t st = read_repos_conf(path, &cfg, &repos); if (st != GL_CONF_OK) return st; st = GL_CONF_ERR_MISSING; int n = config_setting_length(repos); for (int i = 0; i < n; i++) { config_setting_t *e = config_setting_get_elem(repos, (unsigned)i); if (strcmp(config_setting_get_string( config_setting_get_member(e, "name")), repo) != 0) continue; config_setting_t *keys = config_setting_get_member(e, "keys"); size_t used = 0; int nk = config_setting_length(keys); for (int k = 0; k < nk; k++) { const char *key = config_setting_get_string( config_setting_get_elem(keys, (unsigned)k)); size_t len = strlen(key); if (used + len + 2 > outlen) { out[0] = '\0'; st = GL_CONF_ERR_SPACE; goto out; } memcpy(out + used, key, len); out[used + len] = '\n'; used += len + 1; out[used] = '\0'; } st = GL_CONF_OK; break; } out: config_destroy(&cfg); return st; } /* * Every repository repos.conf lists, in its order (= priority), one * "NAME\tURL\n" line each into out; URL is empty for an entry without * one. The whole file is checked as by gl_conf_repo_keys. * GL_CONF_OK out set (possibly "": no repositories) * GL_CONF_ERR_READ file missing, unreadable, or not valid libconfig * GL_CONF_ERR_INVALID not the format gl_conf_repo_keys describes * GL_CONF_ERR_SPACE the lines don't fit in outlen */ gl_conf_status_t gl_conf_repos(const char *path, char *out, size_t outlen) { if (!out || outlen == 0) return GL_CONF_ERR_SPACE; out[0] = '\0'; config_t cfg; config_setting_t *repos; gl_conf_status_t st = read_repos_conf(path, &cfg, &repos); if (st != GL_CONF_OK) return st; size_t used = 0; int n = config_setting_length(repos); for (int i = 0; i < n; i++) { config_setting_t *e = config_setting_get_elem(repos, (unsigned)i); const char *nm = config_setting_get_string( config_setting_get_member(e, "name")); config_setting_t *url = config_setting_get_member(e, "url"); const char *u = url ? config_setting_get_string(url) : ""; int len = snprintf(out + used, outlen - used, "%s\t%s\n", nm, u); if (len < 0 || (size_t)len >= outlen - used) { out[0] = '\0'; st = GL_CONF_ERR_SPACE; break; } used += (size_t)len; } config_destroy(&cfg); return st; } /* ------------------------------------------------------------------ */ /* Repository indexes (REPO_DESIGN.txt section 4) */ /* ------------------------------------------------------------------ */ /* A version as the store names it: "%d.%d.%d", each part decimal * without leading zeros and fitting an int. Anything else would name a * version directory the store never makes. */ static bool valid_index_ver(const char *v) { for (int part = 0; part < 3; part++) { const char *start = v; long n = 0; while (*v >= '0' && *v <= '9') { n = n * 10 + (*v - '0'); if (n > INT_MAX) return false; v++; } if (v == start || (*start == '0' && v - start > 1)) return false; if (part < 2 && *v++ != '.') return false; } return *v == '\0'; } static bool valid_sha256_field(const char *s) { if (strncmp(s, "sha256:", 7) != 0 || strlen(s) != 7 + 64) return false; for (s += 7; *s; s++) if (!((*s >= '0' && *s <= '9') || (*s >= 'a' && *s <= 'f'))) return false; return true; } /* A group with exactly the members named in `names` (NULL-terminated). */ static bool has_exactly(config_setting_t *g, const char *const *names) { if (!g || config_setting_type(g) != CONFIG_TYPE_GROUP) return false; int n = 0; for (; names[n]; n++) if (!config_setting_get_member(g, names[n])) return false; return config_setting_length(g) == n; } static const char * str_member(config_setting_t *g, const char *name) { config_setting_t *m = config_setting_get_member(g, name); return m && config_setting_type(m) == CONFIG_TYPE_STRING ? config_setting_get_string(m) : NULL; } /* An integer member (int or int64), or false. */ static bool int_member(config_setting_t *g, const char *name, int64_t *out) { config_setting_t *m = config_setting_get_member(g, name); if (!m || (config_setting_type(m) != CONFIG_TYPE_INT && config_setting_type(m) != CONFIG_TYPE_INT64)) return false; *out = (int64_t)config_setting_get_int64(m); return true; } /* DEPS or CONFS: a list of { NAME; VER; } groups, as "NAME VER\n" lines * into a new string (*out; "" for an empty list). */ static bool read_relations(config_setting_t *pkg, const char *name, char **out) { static const char *const members[] = { "NAME", "VER", NULL }; config_setting_t *l = config_setting_get_member(pkg, name); if (!l || config_setting_type(l) != CONFIG_TYPE_LIST) return false; int n = config_setting_length(l); size_t cap = 1, used = 0; for (int i = 0; i < n; i++) { config_setting_t *r = config_setting_get_elem(l, (unsigned)i); const char *nm, *ver; if (!has_exactly(r, members) || !(nm = str_member(r, "NAME")) || !(ver = str_member(r, "VER")) || !gl_valid_pkg_ident(nm) || !valid_index_ver(ver)) return false; cap += strlen(nm) + strlen(ver) + 2; } if (!(*out = malloc(cap))) return false; (*out)[0] = '\0'; for (int i = 0; i < n; i++) { config_setting_t *r = config_setting_get_elem(l, (unsigned)i); used += (size_t)snprintf(*out + used, cap - used, "%s %s\n", str_member(r, "NAME"), str_member(r, "VER")); } return true; } void gl_index_free(gl_index_t *idx) { if (!idx) return; for (size_t i = 0; i < idx->npkgs; i++) { gl_index_pkg_t *p = &idx->pkgs[i]; free(p->name); free(p->ver); free(p->file); free(p->sha256); free(p->deps); free(p->confs); } free(idx->pkgs); free(idx->repo); memset(idx, 0, sizeof(*idx)); } /* * Parse an index's bytes (text[0..len)) into *out, which is zeroed * first; free it with gl_index_free on GL_INDEX_OK. The caller checks * the signature over the SAME bytes first: nothing here is trusted * otherwise, and nothing here checks SERIAL or VALID_UNTIL against * anything (that is the caller's rollback/expiry policy). * * Strict, like repos.conf: exactly REPO, SERIAL (> 0), VALID_UNTIL * (> 0) and PKGS at the top; each package exactly NAME, VER, FILE, * SHA256, SIZE (>= 0), DEPS, CONFS; NAME a gl_valid_pkg_ident; VER as * the store names versions; FILE exactly "NAME-VER.gpkg"; SHA256 * "sha256:" + 64 lowercase hex; DEPS/CONFS lists of { NAME; VER; }; no * NAME + VER twice. A NUL or '@' anywhere is refused before parsing: * an index never has one, and '@' is how libconfig spells @include. * GL_INDEX_OK, GL_INDEX_ERR_FORMAT, GL_INDEX_ERR_REPO (well formed, * but REPO is not `repo`), GL_INDEX_ERR_NOMEM */ gl_index_status_t gl_index_parse(const char *text, size_t len, const char *repo, gl_index_t *out) { static const char *const top[] = { "REPO", "SERIAL", "VALID_UNTIL", "PKGS", NULL }; static const char *const fields[] = { "NAME", "VER", "FILE", "SHA256", "SIZE", "DEPS", "CONFS", NULL }; if (!out) return GL_INDEX_ERR_FORMAT; memset(out, 0, sizeof(*out)); if (!text || !repo || memchr(text, '\0', len) || memchr(text, '@', len)) return GL_INDEX_ERR_FORMAT; char *buf = malloc(len + 1); if (!buf) return GL_INDEX_ERR_NOMEM; memcpy(buf, text, len); buf[len] = '\0'; config_t cfg; config_init(&cfg); int ok = config_read_string(&cfg, buf); free(buf); if (!ok) { config_destroy(&cfg); return GL_INDEX_ERR_FORMAT; } gl_index_status_t st = GL_INDEX_ERR_FORMAT; config_setting_t *root = config_root_setting(&cfg); const char *r = str_member(root, "REPO"); config_setting_t *pkgs = config_setting_get_member(root, "PKGS"); if (config_setting_length(root) != 4 || !has_exactly(root, top) || !r || !gl_valid_pkg_ident(r) || !int_member(root, "SERIAL", &out->serial) || out->serial <= 0 || !int_member(root, "VALID_UNTIL", &out->valid_until) || out->valid_until <= 0 || config_setting_type(pkgs) != CONFIG_TYPE_LIST) goto fail; size_t n = (size_t)config_setting_length(pkgs); if (n && !(out->pkgs = calloc(n, sizeof(*out->pkgs)))) { st = GL_INDEX_ERR_NOMEM; goto fail; } for (size_t i = 0; i < n; i++) { config_setting_t *e = config_setting_get_elem(pkgs, (unsigned)i); gl_index_pkg_t *p = &out->pkgs[i]; out->npkgs = i + 1; const char *nm, *ver, *file, *sha; if (!has_exactly(e, fields) || !(nm = str_member(e, "NAME")) || !gl_valid_pkg_ident(nm) || !(ver = str_member(e, "VER")) || !valid_index_ver(ver) || !(file = str_member(e, "FILE")) || !(sha = str_member(e, "SHA256")) || !valid_sha256_field(sha) || !int_member(e, "SIZE", &p->size) || p->size < 0) goto fail; char want[PATH_MAX]; snprintf(want, sizeof want, "%s-%s.gpkg", nm, ver); if (strcmp(file, want) != 0) goto fail; for (size_t j = 0; j < i; j++) if (strcmp(out->pkgs[j].name, nm) == 0 && strcmp(out->pkgs[j].ver, ver) == 0) goto fail; if (!(p->name = strdup(nm)) || !(p->ver = strdup(ver)) || !(p->file = strdup(file)) || !(p->sha256 = strdup(sha))) { st = GL_INDEX_ERR_NOMEM; goto fail; } if (!read_relations(e, "DEPS", &p->deps) || !read_relations(e, "CONFS", &p->confs)) goto fail; } if (strcmp(r, repo) != 0) { st = GL_INDEX_ERR_REPO; goto fail; } if (!(out->repo = strdup(r))) { st = GL_INDEX_ERR_NOMEM; goto fail; } config_destroy(&cfg); return GL_INDEX_OK; fail: config_destroy(&cfg); gl_index_free(out); return st; }