From 70e0f7eba69b3a353effe0a619904366bd935969 Mon Sep 17 00:00:00 2001 From: Taylor R Campbell Date: Sun, 4 Jan 2026 17:02:55 +0000 Subject: [PATCH 1/2] Use an application id and user version number for schema. Rather than having to craft a new CHECK_DB_LATEST that fails with all old schema versions and works with the new schema version every time we make a schema change, we can just change the user_version number. --- pkgin.sql | 3 +++ pkgindb.c | 34 +++++++++++++++++++++++++++++++++- pkgindb_queries.c | 11 ----------- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/pkgin.sql b/pkgin.sql index 748cb85..817f163 100644 --- a/pkgin.sql +++ b/pkgin.sql @@ -195,3 +195,6 @@ CREATE INDEX [idx_local_pkg_comment] ON [LOCAL_PKG] ( CREATE INDEX [idx_local_pkg_name] ON [LOCAL_PKG] ( [PKGNAME] ASC ); + +PRAGMA application_id = 1886087022; +PRAGMA user_version = 1; diff --git a/pkgindb.c b/pkgindb.c index da7ffb5..af1f9d2 100644 --- a/pkgindb.c +++ b/pkgindb.c @@ -233,6 +233,31 @@ pkgindb_savepoint_release(uint64_t savepoint) } } +/* + * Check the application_id and user_version parameters. + */ +static int +check_appid(void *param, int argc, char **argv, char **colname) +{ + + if (argc != 1 || argv == NULL) + return PDB_ERR; + if (strcmp(argv[0], "1886087022") != 0) + return PDB_ERR; + return PDB_OK; +} + +static int +check_version(void *param, int argc, char **argv, char **colname) +{ + + if (argc != 1 || argv == NULL) + return PDB_ERR; + if (strcmp(argv[0], "1") != 0) + return PDB_ERR; + return PDB_OK; +} + /* * Configure the pkgin database. Returns 0 if opening an existing compatible * database, or 1 if the database needs to be created or recreated (in the case @@ -277,7 +302,14 @@ pkgindb_open(void) errx(EXIT_FAILURE, "cannot create database: %s", sqlite3_errmsg(pdb)); } else { - if (pkgindb_doquery(CHECK_DB_LATEST, NULL, NULL) != PDB_OK) { + if (pkgindb_doquery("PRAGMA application_id", check_appid, NULL) + != PDB_OK) { + if (unlink(pkgin_sqldb) < 0) + err(EXIT_FAILURE, "cannot recreate database"); + goto recreate; + } + if (pkgindb_doquery("PRAGMA user_version", check_version, NULL) + != PDB_OK) { if (unlink(pkgin_sqldb) < 0) err(EXIT_FAILURE, "cannot recreate database"); goto recreate; diff --git a/pkgindb_queries.c b/pkgindb_queries.c index 96688f1..05898d5 100644 --- a/pkgindb_queries.c +++ b/pkgindb_queries.c @@ -27,17 +27,6 @@ * SUCH DAMAGE. */ -/* - * This query checks the compatibility of the current database, and should be - * one that either completes or fails due to an SQL error based on the most - * recent schema change. Returned rows are ignored, so choose a query that - * runs quickly. - */ -const char CHECK_DB_LATEST[] = - "SELECT pkgbase " - " FROM local_conflicts " - " LIMIT 1;"; - const char DELETE_LOCAL[] = "DELETE FROM LOCAL_PKG;" "DELETE FROM LOCAL_CONFLICTS;" From 204174b55a95b06f2f0a881f5c484d8b346e93b7 Mon Sep 17 00:00:00 2001 From: Taylor R Campbell Date: Tue, 6 Jan 2026 02:08:51 +0000 Subject: [PATCH 2/2] Verify FILE_CKSUM (sha256) in pkg_summary(5) on download. This is important for detecting version rollback attacks -- verifying a signature on the package itself doesn't help, because the old one also has a valid signature. --- actions.c | 27 ++++++++++++++++++++++++--- download.c | 25 ++++++++++++++++++++++++- pkgin.h | 4 +++- pkgin.sql | 4 +++- pkgindb.c | 2 +- pkgindb_queries.c | 5 +++-- pkglist.c | 5 ++++- summary.c | 18 +++++++++++++++--- 8 files changed, 77 insertions(+), 13 deletions(-) diff --git a/actions.c b/actions.c index 41a0cc4..5ec6d11 100644 --- a/actions.c +++ b/actions.c @@ -39,6 +39,12 @@ #include #endif +#ifdef HAVE_NBCOMPAT_H +#include +#else +#include +#endif + static int warn_count = 0, err_count = 0; static uint8_t said = 0; FILE *err_fp = NULL; @@ -82,6 +88,8 @@ pkg_download(Plisthead *installhead) (void) umask(DEF_UMASK); if (strncmp(p->ipkg->pkgurl, "file:///", 8) == 0) { + char sha256[SHA256_DIGEST_STRING_LENGTH]; + /* * If this package repository URL is file:// we can * just symlink rather than copying. We do not support @@ -99,6 +107,13 @@ pkg_download(Plisthead *installhead) continue; } + if (p->ipkg->rpkg->sha256) { + if (SHA256_File(pkgurl, sha256) == NULL || + strcmp(sha256, p->ipkg->rpkg->sha256) != 0) + errx(EXIT_FAILURE, "Hash mismatch %s", + p->ipkg->pkgfs); + } + if (symlink(pkgurl, p->ipkg->pkgfs) < 0) { errx(EXIT_FAILURE, "Failed to create symlink %s", @@ -116,7 +131,8 @@ pkg_download(Plisthead *installhead) err(EXIT_FAILURE, MSG_ERR_OPEN, p->ipkg->pkgfs); p->ipkg->file_size = - download_pkg(p->ipkg->pkgurl, fp, i++, count); + download_pkg(p->ipkg->pkgurl, fp, i++, count, + p->ipkg->rpkg->sha256); if (p->ipkg->file_size == -1) { (void) fclose(fp); @@ -613,6 +629,7 @@ pkgin_install(char **pkgargs, int do_inst, int upgrade) char **corepkgs; char pkgrepo[BUFSIZ], query[BUFSIZ]; char h_psize[H_BUF], h_fsize[H_BUF], h_free[H_BUF]; + char sha256[SHA256_DIGEST_STRING_LENGTH]; struct stat st; if (is_empty_remote_pkglist()) { @@ -720,12 +737,16 @@ pkgin_install(char **pkgargs, int do_inst, int upgrade) /* * If the binary package has not already been downloaded, or - * its size does not match pkg_summary, then mark it to be + * its size/hash do not match pkg_summary, then mark it to be * downloaded. */ if (stat(p->pkgfs, &st) < 0 || st.st_size != p->rpkg->file_size) p->download = 1; - else { + else if (p->rpkg->sha256 != NULL && + SHA256_File(p->pkgfs, sha256) != NULL && + strcmp(p->rpkg->sha256, sha256) == 0) { + /* exact match, no further check needed */ + } else { /* * If the cached package has the correct size, we must * verify that the BUILD_DATE has not changed, in case diff --git a/download.c b/download.c index f6d1414..e7007c8 100644 --- a/download.c +++ b/download.c @@ -30,6 +30,12 @@ #include "pkgin.h" #include "external/progressmeter.h" +#ifdef HAVE_NBCOMPAT_H +#include +#else +#include +#endif + extern char fetchflags[3]; /* @@ -159,7 +165,8 @@ sum_close(struct archive *a, void *data) * Download a package to the local cache. */ off_t -download_pkg(char *pkg_url, FILE *fp, int cur, int total) +download_pkg(char *pkg_url, FILE *fp, int cur, int total, + const char *sha256_expected) { struct url_stat st; size_t size, wrote; @@ -168,6 +175,8 @@ download_pkg(char *pkg_url, FILE *fp, int cur, int total) struct url *url; fetchIO *f = NULL; char buf[4096]; + SHA256_CTX sha256; + char sha256_actual[SHA256_DIGEST_STRING_LENGTH]; char *pkg, *ptr, *msg = NULL; if ((url = fetchParseURL(pkg_url)) == NULL) @@ -194,6 +203,9 @@ download_pkg(char *pkg_url, FILE *fp, int cur, int total) start_progress_meter(msg, st.size, &statsize); } + if (sha256_expected) + SHA256_Init(&sha256); + while (written < st.size) { if ((fetched = fetchIO_read(f, buf, sizeof(buf))) == 0) break; @@ -208,6 +220,8 @@ download_pkg(char *pkg_url, FILE *fp, int cur, int total) statsize += fetched; size = (size_t)fetched; + if (sha256_expected) + SHA256_Update(&sha256, (const void *)buf, size); for (ptr = buf; size > 0; ptr += wrote, size -= wrote) { if ((wrote = fwrite(ptr, 1, size, fp)) < size) { if (ferror(fp) && errno == EINTR) @@ -233,5 +247,14 @@ download_pkg(char *pkg_url, FILE *fp, int cur, int total) return -1; } + if (sha256_expected) { + SHA256_End(&sha256, sha256_actual); + if (strcmp(sha256_expected, sha256_actual) != 0) { + fprintf(stderr, "download error: %s corrupted\n", + pkg_url); + return -1; + } + } + return written; } diff --git a/pkgin.h b/pkgin.h index 02eb4c0..6ad55cc 100644 --- a/pkgin.h +++ b/pkgin.h @@ -308,6 +308,8 @@ typedef struct Pkglist { int skip; /* Already processed via a different path */ int keep; /*!< autoremovable package ? */ + char *sha256; + SLIST_ENTRY(Pkglist) next; } Pkglist; @@ -361,7 +363,7 @@ Sumfile *sum_open(char *, time_t *); int sum_start(struct archive *, void *); ssize_t sum_read(struct archive *, void *, const void **); int sum_close(struct archive *, void *); -off_t download_pkg(char *, FILE *, int, int); +off_t download_pkg(char *, FILE *, int, int, const char *); /* summary.c */ int update_db(int, int); void split_repos(void); diff --git a/pkgin.sql b/pkgin.sql index 817f163..2d6f5ad 100644 --- a/pkgin.sql +++ b/pkgin.sql @@ -25,6 +25,7 @@ CREATE TABLE [REMOTE_PKG] ( "SIZE_PKG" TEXT, "FILE_SIZE" TEXT, "OPSYS" TEXT, + "FILE_CKSUM sha256" TEXT, "REPOSITORY" TEXT ); @@ -45,6 +46,7 @@ CREATE TABLE [LOCAL_PKG] ( "SIZE_PKG" TEXT, "FILE_SIZE" TEXT, "OPSYS" TEXT, + "FILE_CKSUM sha256" TEXT, "PKG_KEEP" INTEGER NULL ); @@ -197,4 +199,4 @@ CREATE INDEX [idx_local_pkg_name] ON [LOCAL_PKG] ( ); PRAGMA application_id = 1886087022; -PRAGMA user_version = 1; +PRAGMA user_version = 2; diff --git a/pkgindb.c b/pkgindb.c index af1f9d2..b91d2e2 100644 --- a/pkgindb.c +++ b/pkgindb.c @@ -253,7 +253,7 @@ check_version(void *param, int argc, char **argv, char **colname) if (argc != 1 || argv == NULL) return PDB_ERR; - if (strcmp(argv[0], "1") != 0) + if (strcmp(argv[0], "2") != 0) return PDB_ERR; return PDB_OK; } diff --git a/pkgindb_queries.c b/pkgindb_queries.c index 05898d5..4f366ec 100644 --- a/pkgindb_queries.c +++ b/pkgindb_queries.c @@ -105,14 +105,15 @@ const char UNKEEP_PKG[] = /* for upgrades, prefer higher versions to be at the top of SLIST */ const char LOCAL_PKGS_QUERY_ASC[] = "SELECT FULLPKGNAME,PKGNAME,PKGVERS,BUILD_DATE," - "COMMENT,FILE_SIZE,SIZE_PKG,CATEGORIES,PKGPATH,PKG_KEEP " + "COMMENT,FILE_SIZE,SIZE_PKG,CATEGORIES,PKGPATH,\"FILE_CKSUM sha256\"," + "PKG_KEEP " "FROM LOCAL_PKG " "ORDER BY FULLPKGNAME ASC;"; /* present packages by repository appearance to avoid conflicts between repos */ const char REMOTE_PKGS_QUERY_ASC[] = "SELECT FULLPKGNAME,PKGNAME,PKGVERS,BUILD_DATE," - "COMMENT,FILE_SIZE,SIZE_PKG,CATEGORIES,PKGPATH " + "COMMENT,FILE_SIZE,SIZE_PKG,CATEGORIES,PKGPATH,\"FILE_CKSUM sha256\" " "FROM REMOTE_PKG " "INNER JOIN REPOS WHERE REMOTE_PKG.REPOSITORY = REPOS.REPO_URL " "ORDER BY REPOS.ROWID, FULLPKGNAME ASC;"; diff --git a/pkglist.c b/pkglist.c index e5e2ed2..7242682 100644 --- a/pkglist.c +++ b/pkglist.c @@ -103,6 +103,7 @@ malloc_pkglist(void) pkglist->pkgpath = NULL; pkglist->skip = 0; pkglist->keep = 0; + pkglist->sha256 = NULL; pkglist->action = ACTION_NONE; return pkglist; @@ -118,6 +119,7 @@ free_pkglist_entry(Pkglist **plist) { int i; + XFREE((*plist)->sha256); XFREE((*plist)->pkgfs); XFREE((*plist)->pkgurl); XFREE((*plist)->full); @@ -189,11 +191,12 @@ record_pkglist(void *param, int argc, char **argv, char **colname) NUM_OR_NULL(p->size_pkg, argv[6]); DUP_OR_NULL(p->category, argv[7]); DUP_OR_NULL(p->pkgpath, argv[8]); + DUP_OR_NULL(p->sha256, argv[9]); /* * Only LOCAL_PKG has PKG_KEEP. */ - if (plist->P_type == 0 && argv[9]) + if (plist->P_type == 0 && argv[10]) p->keep = 1; if (plist->P_type == 1) { diff --git a/summary.c b/summary.c index 5ca7019..44ec611 100644 --- a/summary.c +++ b/summary.c @@ -224,7 +224,7 @@ prepare_insert(int pkgid, struct Summary sum) /* insert fields */ SLIST_FOREACH(pi, &inserthead, next) { - sqlite3_snprintf(sizeof(tmpbuf), tmpbuf, ",%w", pi->field); + sqlite3_snprintf(sizeof(tmpbuf), tmpbuf, ",\"%w\"", pi->field); if (strlcat(querybuf, tmpbuf, sizeof(querybuf)) >= sizeof(querybuf)) goto err; @@ -354,7 +354,12 @@ parse_entry(struct Summary sum, int pkgid, char *line) * Handle remaining columns. */ for (i = 0; i < cols.num; i++) { - snprintf(buf, BUFSIZ, "%s=", cols.name[i]); + if (strncmp(cols.name[i], "FILE_CKSUM ", 11) == 0) { + snprintf(buf, BUFSIZ, "FILE_CKSUM=%s ", + cols.name[i] + 11); + } else { + snprintf(buf, BUFSIZ, "%s=", cols.name[i]); + } if (strncmp(buf, line, strlen(buf)) == 0) { /* Split PKGNAME into parts */ @@ -373,8 +378,15 @@ parse_entry(struct Summary sum, int pkgid, char *line) *v++ = '\0'; add_to_slist("PKGNAME", val); add_to_slist("PKGVERS", v); - } else + } else { + if (strncmp(cols.name[i], "FILE_CKSUM ", 11) + == 0) { + if ((val = strchr(val, ' ')) == NULL) + return; + val++; + } add_to_slist(cols.name[i], val); + } break; } }