/*- * Copyright 2010. Ivan Voras * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. */ /* * FreeBSD install - a package for the installation and maintainance * of non-core utilities. * * This is the module for applying patches to live systems. */ #include __FBSDID("$FreeBSD$"); #include #include #include #include #include #include #include #include #include #include #include #include "pkg_patch.h" #include "applypatch.h" #include "hashjob.h" /* * Create a backup package (pkg_create -b) for the given package (identified * by its full name, e.g. "apache-2.2.13"). */ static int pkg_backup(char *name, char *pkg_file) { if (!isinstalledpkg(name)) { warnx("Package not installed: %s", name); return (-1); } if (access(PKGPATCH_BACKUP_DIR, F_OK) != 0) { if (mkdir(PKGPATCH_BACKUP_DIR, 0730) != 0) { warnx("Cannot mkdir: %s", PKGPATCH_BACKUP_DIR); return (-1); } } snprintf(pkg_file, PATH_MAX, "%s/%s.%s", PKGPATCH_BACKUP_DIR, name, PKG_FORMAT_EXT); if (vsystem("%s -b %s %s", _PATH_PKG_CREATE, name, pkg_file) != 0) { warnx("pkg_create -b %s %s failed", name, pkg_file); return (-1); } if (Verbose) printf("Created backup package: %s\n", pkg_file); return (0); } /* * Read the given +CONTENTS file. */ static int read_package_contents_file(char *pfilename, Package *pkg) { FILE *fp; fp = fopen(pfilename, "r"); if (fp == NULL) { warnx("Cannot open file: %s", pfilename); return (-1); } read_plist(pkg, fp); return (0); } /* * Read live/installed package metadata. The package is identified by its full * name (e.g. "apache-2.2.13"). */ static int read_package_by_name(char *name, Package *pkg) { char pfilename[PATH_MAX]; snprintf(pfilename, PATH_MAX, "%s/%s/%s", LOG_DIR, name, CONTENTS_FNAME); return (read_package_contents_file(pfilename, pkg)); } /* * Convert a package-relative filename to the absolute (live) filename, using * the "cwd" information from the Package plist information. */ static int pkg_to_live_filename(char *afilename, char *pfilename, Package *pkg, const char *msg) { char *last_cwd = NULL; PackingList pli; Boolean found = FALSE; pli = pkg->head; while (pli != NULL) { if (pli->type == PLIST_CWD) last_cwd = pli->name; if (pli->type == PLIST_FILE && strncmp(pfilename, pli->name, PATH_MAX) == 0) { found = TRUE; break; } pli = pli->next; } if (!found) { warnx("File not found in package metadata: %s [%s]", pfilename, msg); return (-1); } if (last_cwd == NULL) { warnx("Failure to detect package cwd info [%s]", msg); return (-1); } snprintf(afilename, PATH_MAX, "%s/%s", last_cwd, pli->name); return (0); } /* * Convert a package-relative directory name to the absolute (live) directory * name. This is actually somewhat evil and heuristic because the package * metadata doesn't reference directories and we try to find any file whose * path begins with the given directory. This should work for all official * packages but as the metadata format is badly designed, there can be lots of * edge cases. Solving those is left as an excercise for the reader. */ static int pkg_to_live_dirname(char *adirname, char *pdirname, Package *pkg, const char *msg) { PackingList pli; char *last_cwd = NULL; int pdirlen; Boolean found = FALSE; *adirname = '\0'; pdirlen = strlen(pdirname); pli = pkg->head; while (pli != NULL) { if (pli->type == PLIST_CWD) last_cwd = pli->name; if (pli->type == PLIST_FILE && strncmp(pdirname, pli->name, pdirlen) == 0) { /* Just find anything which begins with the pdirname */ found = TRUE; break; } pli = pli->next; } if (!found) { warnx("Directory cannot be divined from package metadata: " "%s [%s]", pdirname, msg); return (-1); } if (last_cwd == NULL) { warnx("Failure to detect package cwd info [%s]", msg); return (-1); } snprintf(adirname, PATH_MAX, "%s/%s", last_cwd, pdirname); return (0); } /* * Finds a MD5 comment for the given file within the given package metadata. * As the package metadata is informally specified, nothing guarantees this * will succeed :) * The MD5 comment is expected to be after the file node and before any * following file node. */ static int pkg_find_md5(char *filename, Package *pkg, char *md5) { PackingList pli; Boolean found = FALSE; char *p; pli = pkg->head; *md5 = '\0'; while (pli != NULL) { if (pli->type == PLIST_FILE) { if (strncmp(filename, pli->name, PATH_MAX) == 0) { found = TRUE; break; } } pli = pli->next; } if (!found) return (-1); found = FALSE; while (pli != NULL) { if (pli->type == PLIST_COMMENT) { p = strchr(pli->name, ':'); if (p != NULL && strncmp(pli->name, "MD5:", 4) == 0) { strlcpy(md5, p+1, 33); found = TRUE; break; } } pli = pli->next; if (pli != NULL) if (pli->type == PLIST_FILE) return (-1); } if (!found) return (-1); return (0); } /* * Rudely overwrites metadata files in the package given by target_pkg_name * with the ones in sourcedir referenced by package metadata pkg. */ static int pkg_install_metadata(char *sourcedir, Package *pkg, char *target_pkg_name) { PackingList pli; char sourcef[PATH_MAX], destf[PATH_MAX]; Boolean copied_contents = FALSE; pli = pkg->head; while (pli != NULL) { if (pli->type == PLIST_FILE && pli->name[0] == '+') { snprintf(sourcef, PATH_MAX, "%s/%s", sourcedir, pli->name); if (access(sourcef, R_OK) != 0) { warn("Cannot access file: %s", sourcef); return (-1); } snprintf(destf, PATH_MAX, "%s/%s/%s", LOG_DIR, target_pkg_name, pli->name); if (copy_file_absolute(sourcef, destf) != 0) { warn("Cannot copy %s to %s", sourcef, destf); return (-1); } if (Verbose > 1) printf("Copied %s\n", pli->name); if (strcmp(pli->name, "+CONTENTS") == 0) copied_contents = TRUE; } pli = pli->next; } if (!copied_contents) { /* * XXX: Why isn't +CONTENTS mentioned in itself? Possibly * because it would be hard to include its own md5 hash, but * the hash is optional so it would be cleaner to include it * than to handle it as a special case. */ snprintf(sourcef, PATH_MAX, "%s/%s", sourcedir, "+CONTENTS"); snprintf(destf, PATH_MAX, "%s/%s/%s", LOG_DIR, target_pkg_name, "+CONTENTS"); if (copy_file_absolute(sourcef, destf) != 0) { warn("Cannot copy %s to %s", sourcef, destf); return (-1); } if (Verbose > 1) printf("Copied %s\n", "+CONTENTS"); copied_contents = TRUE; } return (0); } /* * Apply patch command */ void perform_applypatch(char *file_patch) { char fpatch[PATH_MAX], dpatch[PATH_MAX], tmp[PATH_MAX], pext[10]; char backup_pkg[PATH_MAX]; struct pkgxjob xpatch; struct pkg_patch pp; struct pplist *pl; Package pkg_live, pkg_new; enum CMP_NAME depcheck; FILE **fpvect; unsigned int err_count, n_patched_files, i; baton_twirl(); if (realpath(file_patch, fpatch) == NULL) err(1, "Error resolving path: %s", file_patch); if (access(fpatch, F_OK) != 0) err(1, "File not found: %s", fpatch); if (access(fpatch, R_OK) != 0) err(1, "Access error reading file: %s", fpatch); snprintf(dpatch, PATH_MAX, "%s/patch", my_tmp); if (access(dpatch, R_OK) == 0) rm_rf(dpatch); if (mkdir(dpatch, 0700) != 0) err(1, "Cannot create directory: %s", dpatch); if (pkgxjob_start(&xpatch, dpatch, fpatch) != 0) err(1, "Cannot extract package %s to %s (start)", fpatch, dpatch); if (pkgxjob_finish(&xpatch) != 0) err(1, "Cannot extract package %s to %s (finish)", fpatch, dpatch); snprintf(tmp, PATH_MAX, "%s/%s", dpatch, PKGPATCH_FNAME); err_count = 0; /* Step 1 - read the patch metadata */ read_pkgpatch_file(tmp, &pp); if (pp.version_major != PKGPATCH_VERSION_MAJOR) errx(1, "Invalid patch data format major version number: %d\n", pp.version_major); if (pp.version_minor > PKGPATCH_VERSION_MINOR) errx(1, "Invalid patch data format minor version number: %d\n", pp.version_minor); if (Verbose) printf("Parsed patch version %d.%d for '%s' to '%s'\n", pp.version_major, pp.version_minor, pp.source, pp.target); if (Verbose > 1) printf("New files: %u, removed: %u, changed: %u, rmdirs: %u\n", pplist_count(&pp.pp_add), pplist_count(&pp.pp_remove), pplist_count(&pp.pp_patch), pplist_count(&pp.pp_rmdir)); if (!isinstalledpkg(pp.source)) errx(1, "The patch file applies to package '%s' which doesn't " "appear to be installed.", pp.source); /* * Step 2 - read the existing (live system) package data and the new * package data. */ memset(&pkg_live, 0, sizeof(pkg_live)); memset(&pkg_new, 0, sizeof(pkg_new)); memset(&xpatch, 0, sizeof(xpatch)); if (read_package_by_name(pp.source, &pkg_live) != 0) err(1, "Cannot read package information for %s", pp.source); snprintf(tmp, PATH_MAX, "%s/%s", dpatch, CONTENTS_FNAME); if (read_package_contents_file(tmp, &pkg_new) != 0) err(1, "Cannot read package information from %s", tmp); /* Check for conflicts from the new package */ check_conflicts(&pkg_new, NULL); /* Step 3 - verify that the live system and the patch file agree */ if (Verbose > 1) printf("Verifying live system and patch data consistency...\n"); /* Check that files to be added don't exist already. */ STAILQ_FOREACH(pl, &pp.pp_add, linkage) { snprintf(tmp, PATH_MAX, "%s/%s", dpatch, pl->filename); if (isdir(tmp)) continue; if (pkg_to_live_filename(tmp, pl->filename, &pkg_new, "pp_add") != 0) { err_count++; continue; } if (access(tmp, F_OK) == 0) { warnx("File exists but shouldn't: %s", tmp); /* err_count++; */ } } /* Check that files to be removed actually exist. */ STAILQ_FOREACH(pl, &pp.pp_remove, linkage) { if (pkg_to_live_filename(tmp, pl->filename, &pkg_live, "pp_remove") != 0) { err_count++; continue; } if (access(tmp, F_OK) != 0) { warnx("File should exist but doesn't: %s", tmp); err_count++; } } /* Check that directories to be removed actually exist. */ STAILQ_FOREACH(pl, &pp.pp_rmdir, linkage) { if (pkg_to_live_dirname(tmp, pl->filename, &pkg_live, "pp_rmdir") != 0) { err_count++; continue; } if (access(tmp, F_OK) != 0) { warnx("Directory should exist but doesn't: %s", tmp); err_count++; } } /* Check that files to be patched actually exist. */ STAILQ_FOREACH(pl, &pp.pp_patch, linkage) { if (pkg_to_live_filename(tmp, pl->filename, &pkg_live, "pp_patch") != 0) { err_count++; continue; } if (access(tmp, F_OK) != 0) { warnx("File should exist but doesn't: %s", tmp); err_count++; } } /* Check dependancies of the new package */ depcheck = check_dependencies(&pkg_new, NULL); if (depcheck == CMP_NO_MATCH || depcheck == CMP_BASE_MATCH) printf("Dependancy error processing %s:\n", file_patch); if (depcheck == CMP_NO_MATCH) { if (!Force) errx(1, "New package dependancies not satisfied"); else warnx("New package dependancies not satisfied" " -- forced continue"); } if (depcheck == CMP_BASE_MATCH) { printf("New package dependancies weakly matched - version " "mismatch.\n"); if (!Force) { if (!y_or_n(0, "Continue?")) errx(1, "Aborted."); } else printf("Forced continue.\n"); } if (err_count != 0) { if (!Force) errx(1, "Found %u errors.", err_count); else warnx("Found %u errors. Forcing continue.", err_count); } baton_twirl(); /* Step 4 - backup the existing package */ if (Verbose > 1) printf("Backing up the old package...\n"); if (pkg_backup(pp.source, backup_pkg) != 0) err(1, "Cannot backup package: %s", pp.source); /* * Step 5 - apply patches. This is done by generating new files from * existing files and patches, then verifying all of them match expected * checksum, then rename()-ing them to the expected files. */ if (Verbose > 1) printf("Applying patches...\n"); snprintf(pext, sizeof(pext), ".p%u", getpid()); fpvect = calloc(pplist_count(&pp.pp_patch), sizeof(*fpvect)); n_patched_files = 0; STAILQ_FOREACH(pl, &pp.pp_patch, linkage) { char newfile[PATH_MAX], patchfile[PATH_MAX]; baton_twirl(); if (pl->filename[0] == '+') continue; if (pkg_to_live_filename(tmp, pl->filename, &pkg_live, "pp_patch2") != 0) { err_count++; warnx("Cannot resolve %s on a live pkg", pl->filename); } snprintf(newfile, PATH_MAX, "%s%s", tmp, pext); if (pl->method == PPMETHOD_CP || pl->method == PPMETHOD_LN) snprintf(patchfile, PATH_MAX, "%s/%s", dpatch, pl->filename); else if (pl->method == PPMETHOD_BSDIFF) snprintf(patchfile, PATH_MAX, "%s/%s.%s", dpatch, pl->filename, BSDIFF_EXT); else errx(1, "Unknown patch method: %d", (int)(pl->method)); if (Verbose > 2) printf("Patching %s to %s using %s\n", tmp, newfile, patchfile); if (pl->method == PPMETHOD_BSDIFF) { char cmd[3 * PATH_MAX]; snprintf(cmd, sizeof(cmd), "%s \"%s\" \"%s\" \"%s\"", _PATH_BSPATCH, tmp, newfile, patchfile); fpvect[n_patched_files] = popen(cmd, "r+"); if (fpvect[n_patched_files] == NULL) err(1, "Cannot popen: %s", cmd); n_patched_files++; } else if (pl->method == PPMETHOD_LN) { if (copy_file_absolute(patchfile, newfile) != 0) err(1, "Cannot symlink %s to %s", patchfile, newfile); } else { if (copy_file_absolute(patchfile, newfile) != 0) err(1, "Cannot copy %s to %s", patchfile, newfile); } } for (i = 0; i < n_patched_files; i++) if (pclose(fpvect[i]) != 0) err(1, "pclose() failed"); /* Verify patched files are correct */ if (Verbose > 1) printf("Verifying patched files...\n"); STAILQ_FOREACH(pl, &pp.pp_patch, linkage) { char live_md5[33], target_md5[33], newfile[PATH_MAX]; if (pl->filename[0] == '+') continue; if (pkg_find_md5(pl->filename, &pkg_new, target_md5) != 0) { warnx("Cannot find MD5 of %s in target metadata", pl->filename); continue; } if (pkg_to_live_filename(newfile, pl->filename, &pkg_live, "pp_patch3") != 0) { err_count++; warnx("Cannot resolve %s on live pkg for verifying", pl->filename); break; } strncat(newfile, pext, PATH_MAX); if (issymlink(newfile)) /* Symlinks are relative and point to wrong files at this point */ continue; if (MD5File(newfile, live_md5) == NULL) err(1, "Cannot MD5 file: %s", newfile); if (strncmp(live_md5, target_md5, sizeof(live_md5)) != 0) { warnx("MD5 mismatch for %s: expected %s, got %s", newfile, target_md5, live_md5); abort(); goto error_cleanup; } if (pl->method == PPMETHOD_BSDIFF) snprintf(tmp, PATH_MAX, "%s/%s.%s", dpatch, pl->filename, BSDIFF_EXT); else snprintf(tmp, PATH_MAX, "%s/%s", dpatch, pl->filename); if (copy_file_attrs(tmp, NULL, newfile) != 0) { warn("Cannot copy file attributes from %s to %s", tmp, newfile); goto error_cleanup; } baton_twirl(); } /* All is well, we can rename() the new files to the live ones. */ if (Verbose > 1) printf("Finishing...\n"); STAILQ_FOREACH(pl, &pp.pp_patch, linkage) { char newfile[PATH_MAX], livefile[PATH_MAX]; if (pl->filename[0] == '+') continue; if (pkg_to_live_filename(livefile, pl->filename, &pkg_live, "pp_patch4") != 0) errx(1, "Cannot resolve %s on live pkg", pl->filename); snprintf(newfile, PATH_MAX, "%s%s", livefile, pext); assert(access(newfile, F_OK) == 0); if (rename(newfile, livefile) != 0) { warn("rename(%s,%s) failed", newfile, livefile); goto error_cleanup; } } /* Step 6 - apply other cases - files to add, remove, dirs to rmdir */ STAILQ_FOREACH(pl, &pp.pp_add, linkage) { char livefile[PATH_MAX]; int er; snprintf(tmp, PATH_MAX, "%s/%s", dpatch, pl->filename); if (!isdir(tmp)) er = pkg_to_live_filename(livefile, pl->filename, &pkg_new, "pp_add2"); else er = pkg_to_live_dirname(livefile, pl->filename, &pkg_new, "pp_add2"); if (er != 0) { warnx("Cannot resolve %s in new pkg", pl->filename); goto error_cleanup; } if (copy_file_absolute(tmp, livefile) != 0) { warn("Cannot copy %s to %s [pp_add]", tmp, livefile); goto error_cleanup; } } baton_twirl(); STAILQ_FOREACH(pl, &pp.pp_remove, linkage) { if (pkg_to_live_filename(tmp, pl->filename, &pkg_live, "pp_remove2") != 0) { warnx("Cannot resolve %s on live system", pl->filename); goto error_cleanup; } if (unlink(tmp) != 0) { warn("Cannot unlink: %s -- continuing", tmp); /* goto error_cleanup; */ } } STAILQ_FOREACH(pl, &pp.pp_rmdir, linkage) { if (pkg_to_live_dirname(tmp, pl->filename, &pkg_live, "pp_rmdir2") != 0) { warnx("Cannot resolve directory %s on live system", pl->filename); } if (rmdir(tmp) != 0) { warn("Cannot rmdir: %s -- continuing", tmp); /* goto error_cleanup; */ } } /* * Step 7 - fixup metadata: copy new metadata over the old, then rename * the pkgdb directory. */ if (pkg_install_metadata(dpatch, &pkg_new, pp.source) != 0) { warn("Cannot install new metadata for %s", pp.target); goto error_cleanup; } else { char newpkgdir[PATH_MAX]; /* Rename package db directory */ snprintf(tmp, PATH_MAX, "%s/%s", LOG_DIR, pp.source); snprintf(newpkgdir, PATH_MAX, "%s/%s", LOG_DIR, pp.target); if (rename(tmp, newpkgdir) != 0) { warn("Cannot rename %s to %s", tmp, newpkgdir); goto error_cleanup; } } /* Plaudite, amici, comoedia finita est. */ if (Verbose > 1) printf("All is well.\n"); return; error_cleanup: /* Remove temp patch files, restore backed-up package. */ warnx("Error detected! Rolling back package."); /* Remove any temp patch files */ STAILQ_FOREACH(pl, &pp.pp_patch, linkage) { if (pkg_to_live_filename(tmp, pl->filename, &pkg_new, "pp_patch rollback") == 0) { strncat(tmp, pext, PATH_MAX); if (access(tmp, F_OK) == 0) if (unlink(tmp) != 0) warn("Cannot unlink %s", tmp); } } /* Obliterate old package */ if (vsystem("%s -f %s", _PATH_PKG_DELETE, pp.source) != 0) printf("pkg_delete on %s failed.\n", pp.source); if (vsystem("%s -F %s", _PATH_PKG_ADD, backup_pkg) != 0) { printf("pkg_add on %s failed!\n", backup_pkg); err(1, "Critical! Package rollback probably failed!"); } }