compat-usrmerge/usrmergecheck.c

352 lines
8.3 KiB
C

/*
Copyright (c) 2019,2020 SUSE LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>
#include <limits.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <getopt.h>
#include <libintl.h>
#include <rpm/rpmts.h>
#include <rpm/rpmdb.h>
#include <rpm/rpmlib.h>
#include <fcntl.h>
#include <rpm/rpmcli.h>
#include <rpm/header.h>
#include <rpm/rpmfiles.h>
static int verbose = 0;
const char *dirs[] = {
"/usr/bin",
"/usr/lib",
#if __WORDSIZE == 64
"/usr/lib64",
#endif
"/usr/sbin",
NULL
};
// rpmdb stores dirs with slash
const char *rpmdirs[] = {
"/bin/",
"/lib/",
#if __WORDSIZE == 64
"/lib64/",
#endif
"/sbin/",
NULL
};
static inline int startswith(const char* s, const char* pfx) {
return strncmp(s, pfx, strlen(pfx)) == 0;
}
static inline char stm(mode_t m) {
switch (m & S_IFMT) {
case S_IFBLK: return 'b';
case S_IFCHR: return 'c';
case S_IFDIR: return 'd';
case S_IFIFO: return 'p';
case S_IFLNK: return 'l';
case S_IFREG: return 'f';
case S_IFSOCK: return 'S';
}
return '?';
}
int check_directory(const char* dir);
int check_entry(const char* p)
{
struct stat st, stu;
const char* rp = p+strlen("/usr");
// if the file doesn't exist in /usr we're safe
if(lstat(p, &stu)) {
if (errno != ENOENT) {
perror(p);
return 1;
}
if (verbose > 1) printf("%s unique\n", p);
return 0;
}
if(lstat(rp, &st)) {
perror(rp);
return 1;
}
// differnt file type, check if one is link and can be dropped
if ((st.st_mode & S_IFMT) != (stu.st_mode & S_IFMT) || (S_ISLNK(st.st_mode) && S_ISLNK(stu.st_mode))) {
if (S_ISLNK(st.st_mode) || S_ISLNK(stu.st_mode)) {
struct stat sb1, sb2;
// if the link in / points to the file in /usr it's fine
// XXX: in theory there could be a weird
// chain of links pointing back and forth
// between /usr and /, we ignore that here
if(!stat(rp, &sb1) && !stat(p, &sb2) && sb1.st_ino == sb2.st_ino) {
if (verbose) printf("%s same file, ok\n", rp);
return 0;
}
}
fprintf(stderr, "%s mode mismatch %c vs %c\n", rp, stm(st.st_mode), stm(stu.st_mode));
return 1;
} else {
if (S_ISLNK(st.st_mode)) {
char t1[PATH_MAX] = {0};
char t2[PATH_MAX] = {0};
if(readlink(rp, t1, sizeof(t1)) == -1) {
perror(rp);
return 1;
}
if(readlink(p, t2, sizeof(t2)) == -1) {
perror(p);
return 1;
}
if (!strcmp(t1, t2)) {
if (verbose) {
printf("%s and %s both point %s, ok\n", rp, p, t1);
}
return 0;
} else {
fprintf(stderr, "%s link mismatch %s vs %s\n", rp, t1, t2);
}
} else if (!S_ISDIR(st.st_mode)) {
fprintf(stderr, "%s duplicated\n", rp);
return 1;
}
// both are directories, check recursive
return check_directory(p);
}
}
int check_directory(const char* dir)
{
DIR* dh;
struct dirent* d;
unsigned failed = 0;
dh = opendir(dir+strlen("/usr"));
if (!dh) {
perror(dir);
return 1;
}
while ((d = readdir(dh))) {
char p[PATH_MAX] = {0};
if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
continue;
stpcpy(stpcpy(stpcpy(p, dir), "/"), d->d_name);
failed += check_entry(p);
}
return failed;
}
int check_filesystem(const char* rootdir)
{
unsigned failed = 0;
if (rootdir) {
if (chroot(rootdir)) {
perror("chroot");
return 1;
}
}
for (int i = 0; dirs[i]; ++i) {
struct stat st;
const char* d = dirs[i]+strlen("/usr");
if (lstat(d, &st)) {
if (errno != ENOENT)
perror(d);
continue;
}
if (S_ISDIR(st.st_mode)) {
failed += check_directory(dirs[i]);
} else if (S_ISLNK(st.st_mode)) {
char buf[PATH_MAX] = {0};
if(readlink(d, buf, sizeof(buf)) == -1) {
perror(d);
continue;
}
if (strcmp(buf, dirs[i]+1)) {
fprintf(stderr, "wrong link %s: %s should be %s\n", d, buf, dirs[i]+1);
}
}
}
if (failed) {
fprintf(stderr, ngettext("%u file prevents usrmerge\n", "%u files prevent usrmerge\n", failed), failed);
}
return failed == 0;
}
int rpm_findusrfile(rpmts ts, Header hdr, rpmfi orig_fi)
{
char fn[PATH_MAX] = "/usr";
strcpy(fn+strlen(fn), rpmfiFN(orig_fi));
rpmdbMatchIterator mi = rpmtsInitIterator(ts, RPMDBI_INSTFILENAMES, fn, 0);
if (mi) {
Header h;
int conflict = 1;
while ((h = rpmdbNextIterator(mi)) != NULL) {
rpmfiles files = rpmfilesNew(NULL, hdr, 0, 0);
rpmfi fi = rpmfilesIter(files, 0);
int fx = rpmfiFindFN(fi, fn);
if (fx != -1) {
rpmfiSetFX(fi, fx);
if (S_ISDIR(rpmfiFMode(fi)) && S_ISDIR(rpmfiFMode(orig_fi))) {
conflict = 2;
}
}
rpmfiFree(fi);
rpmfilesFree(files);
}
// we just look at the first one. If there's a
// second match and that is somewhow conflicting the
// system was screwed already.
rpmdbFreeIterator(mi);
return conflict;
}
return 0;
}
int check_rpmdb(char* rootdir)
{
unsigned failed = 0;
rpmcliConfigured();
rpmts ts = rpmtsCreate();
if (!ts) {
fprintf(stderr, "failed to create RPM transaction\n");
return -1;
}
if (rootdir)
rpmtsSetRootDir(ts, rootdir);
if (rpmtsOpenDB(ts, O_RDONLY) != 0) {
fprintf(stderr, "failed to open RPM database\n");
return -1;
}
rpmdbMatchIterator iter = rpmtsInitIterator(ts, RPMDBI_PACKAGES, NULL, 0);
Header hdr;
while ((hdr = rpmdbNextIterator(iter)) != NULL) {
rpmfiles files = rpmfilesNew(NULL, hdr, 0, 0);
rpmfi fi = rpmfilesIter(files, 0);
char skipdir[PATH_MAX] = {0};
int conflict = 0;
while (rpmfiNext(fi) >= 0) {
if (skipdir[0] && startswith(rpmfiFN(fi), skipdir)) {
if (verbose > 2)
printf("skipping %s as %s is known\n", rpmfiFN(fi), skipdir);
continue;
} else {
skipdir[0] = 0;
}
for (int i = 0; rpmdirs[i]; ++i) {
if(startswith(rpmfiODN(fi), rpmdirs[i])) {
rpm_mode_t m = rpmfiFMode(fi);
conflict = rpm_findusrfile(ts, hdr, fi);
if (conflict) {
if (conflict == 2) {
if (verbose > 2) {
fprintf(stderr, "directory %s ok\n", rpmfiFN(fi));
}
conflict = 0;
} else if (verbose > 1) {
char* n = headerGetAsString(hdr, RPMTAG_NEVRA);
fprintf(stderr, "%s: %s conflict\n", n, rpmfiFN(fi));
free(n);
}
} else if (S_ISDIR(m)) {
// an optimization so we don't have to check hundreds
// of kernel modules. If the file at hand is a directory
// and does not exist in /usr we can just skip the rest.
strcpy(skipdir, rpmfiFN(fi));
}
break;
}
}
}
rpmfiFree(fi);
rpmfilesFree(files);
if (conflict) {
++failed;
if (verbose == 1) {
char* n = headerGetAsString(hdr, RPMTAG_NEVRA);
printf("%s breaks\n", n);
free(n);
}
}
}
if (failed) {
fprintf(stderr, ngettext("%u package prevents usrmerge\n", "%u packages prevent usrmerge\n", failed), failed);
}
return failed == 0;
}
int main(int argc, char** argv)
{
enum { fs, rpmdb } mode = fs;
int c;
char* rootdir = NULL;
static struct option long_options[] = {
{"verbose", no_argument, 0, 'v' },
{"rpmdb", no_argument, 0, 128 },
{"root", required_argument, 0, 129 },
{0, 0, 0, 0 }
};
while ((c = getopt_long(argc, argv, "v", long_options, NULL)) != -1) {
switch(c) {
case 'v': ++verbose; break;
case 128: mode = rpmdb; break;
case 129: rootdir=strdup(optarg); break;
}
}
if (mode == rpmdb)
return check_rpmdb(rootdir) == 0;
return check_filesystem(rootdir) == 0;
}