_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file src/image.c
#define _POSIX_C_SOURCE 200809L
#include "image.h"
#include "elf.h"
#include "verify.h"
#include <openssl/evp.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
int holy_image_directory(int parent)
{
return openat(parent, ".", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
}
char *holy_image_path(const char *parent, const char *name)
{
size_t a = strlen(parent), b = strlen(name);
char *path;
if (a > SIZE_MAX - b - 2) return NULL;
path = malloc(a + b + 2);
if (path) sprintf(path, "%s%s%s", parent, a ? "/" : "", name);
return path;
}
void holy_quoted(FILE *out, const char *value)
{
const unsigned char *p = (const unsigned char *)value;
fputc('"', out);
for (; *p; ++p) {
if (*p == '"' || *p == '\\') fprintf(out, "\\%c", *p);
else if (*p < 32 || *p >= 127) fprintf(out, "\\x%02x", *p);
else fputc(*p, out);
}
fputc('"', out);
}
void holy_text_free(struct holy_text *text)
{
free(text->data);
memset(text, 0, sizeof *text);
}
static int text_reserve(struct holy_text *out, size_t extra)
{
if (out->used + extra + 1 > out->capacity) {
size_t next = out->capacity ? out->capacity : 256;
char *grown;
while (next < out->used + extra + 1) {
if (next > SIZE_MAX / 2) return 0;
next *= 2;
}
grown = realloc(out->data, next);
if (!grown) return 0;
out->data = grown;
out->capacity = next;
}
return 1;
}
int holy_text_add(struct holy_text *out, const char *value)
{
size_t length = strlen(value);
if (!text_reserve(out, length)) return 0;
memcpy(out->data + out->used, value, length + 1);
out->used += length;
return 1;
}
int holy_text_read(int dir, const char *name, struct holy_text *out, size_t limit)
{
char buffer[65536];
int file = openat(dir, name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (file < 0) return 0;
for (;;) {
ssize_t got = read(file, buffer, sizeof buffer);
if (got < 0 && errno == EINTR) continue;
if (got < 0) { close(file); return 0; }
if (!got) break;
if (out->used + (size_t)got > limit || !text_reserve(out, (size_t)got)) {
close(file);
return 0;
}
memcpy(out->data + out->used, buffer, (size_t)got);
out->used += (size_t)got;
out->data[out->used] = 0;
}
close(file);
return 1;
}
const char *holy_names_get(const struct holy_names *set, size_t index)
{
return index < set->count ? set->names.data + set->offsets[index] : NULL;
}
int holy_names_has(const struct holy_names *set, const char *name)
{
size_t i;
for (i = 0; i < set->count; ++i)
if (!strcmp(holy_names_get(set, i), name)) return 1;
return 0;
}
/* a name keeps its own terminator in the set, so each offset names a C string */
static int names_append(struct holy_names *set, const char *name)
{
size_t length = strlen(name), offset = set->names.used;
if (!text_reserve(&set->names, length)) return 0;
memcpy(set->names.data + offset, name, length + 1);
set->names.used = offset + length + 1;
return 1;
}
int holy_names_add(struct holy_names *set, const char *name)
{
size_t next, offset;
if (holy_names_has(set, name)) return 1;
if (set->count == set->capacity) {
size_t *offsets;
next = set->capacity ? set->capacity * 2 : 16;
if (next > SIZE_MAX / sizeof *offsets) return 0;
offsets = realloc(set->offsets, next * sizeof *offsets);
if (!offsets) return 0;
set->offsets = offsets;
set->capacity = next;
}
offset = set->names.used;
if (!names_append(set, name)) return 0;
set->offsets[set->count++] = offset;
return 1;
}
static void names_free(struct holy_names *set)
{
holy_text_free(&set->names);
free(set->offsets);
memset(set, 0, sizeof *set);
}
void holy_payload_free(struct holy_payload *payload)
{
size_t i;
for (i = 0; i < payload->count; ++i) {
free((char *)payload->entries[i].path);
free((char *)payload->entries[i].link);
}
free(payload->entries);
free(payload->digests);
names_free(&payload->directories);
names_free(&payload->needed);
names_free(&payload->provided);
names_free(&payload->absolute);
memset(payload, 0, sizeof *payload);
}
static int entry_push(struct holy_payload *out, const char *path, const char *link,
unsigned mode, long long offset, long long size, int directory)
{
struct holy_stream_entry *grown, *slot;
unsigned char (*digests)[32];
if (out->count == out->capacity) {
size_t next = out->capacity ? out->capacity * 2 : 64;
if (next > SIZE_MAX / sizeof *grown) return 0;
grown = realloc(out->entries, next * sizeof *grown);
if (!grown) return 0;
out->entries = grown;
digests = realloc(out->digests, next * sizeof *digests);
if (!digests) return 0;
out->digests = digests;
out->capacity = next;
}
slot = &out->entries[out->count];
memset(slot, 0, sizeof *slot);
memset(out->digests[out->count], 0, sizeof out->digests[0]);
slot->path = strdup(path);
if (!slot->path) return 0;
if (link) {
slot->link = strdup(link);
if (!slot->link) return 0;
}
slot->mode = mode;
slot->uid = out->uid;
slot->gid = out->gid;
slot->offset = offset;
slot->size = size;
slot->directory = directory;
++out->count;
return 1;
}
static int entry_directory(struct holy_payload *out, const char *path)
{
return holy_names_add(&out->directories, path) &&
entry_push(out, path, NULL, 0755, 0, 0, 1);
}
/* every directory an entry sits under is declared by the same manifest, because
the installer refuses a file whose parents it did not place itself */
static int entry_parents(struct holy_payload *out, const char *path)
{
char built[1024];
size_t at;
for (at = 5; path[at]; ++at) {
if (path[at] != '/' || at >= sizeof built) continue;
memcpy(built, path, at);
built[at] = 0;
if (holy_names_has(&out->directories, built)) continue;
if (!entry_directory(out, built)) return 0;
}
return 1;
}
int holy_payload_add(struct holy_payload *out, const char *path, const char *link,
unsigned mode, long long offset, long long size, int directory)
{
if (strncmp(path, "DATA/", 5))
return entry_push(out, path, link, mode, offset, size, directory);
if (!entry_parents(out, path)) return 0;
if (directory) {
if (holy_names_has(&out->directories, path)) return 1;
return entry_directory(out, path);
}
return entry_push(out, path, link, mode, offset, size, directory);
}
int holy_payload_spool_file(struct holy_payload *out, int input, long long *offset,
long long *size, unsigned char digest[32])
{
unsigned char buffer[65536], whole[32];
EVP_MD_CTX *context;
unsigned length = 0;
off_t start = out->written;
int ok = 0;
context = EVP_MD_CTX_new();
if (!context || EVP_DigestInit_ex(context, EVP_sha256(), NULL) != 1) goto done;
for (;;) {
ssize_t got = read(input, buffer, sizeof buffer);
const unsigned char *at = buffer;
size_t left;
if (got < 0 && errno == EINTR) continue;
if (got < 0) goto done;
if (!got) break;
if (EVP_DigestUpdate(context, buffer, (size_t)got) != 1) goto done;
left = (size_t)got;
while (left) {
ssize_t written = write(out->spool, at, left);
if (written < 0 && errno == EINTR) continue;
if (written <= 0) goto done;
at += (size_t)written;
left -= (size_t)written;
out->written += written;
}
}
if (EVP_DigestFinal_ex(context, whole, &length) != 1 || length != sizeof whole) goto done;
memcpy(digest, whole, sizeof whole);
*offset = start;
*size = (long long)(out->written - start);
ok = 1;
done:
EVP_MD_CTX_free(context);
return ok;
}
static int spool_bytes(struct holy_payload *out, const void *data, size_t size,
long long *offset){
const unsigned char *at = data;
*offset = out->written;
while (size) {
ssize_t written = write(out->spool, at, size);
if (written < 0 && errno == EINTR) continue;
if (written <= 0) return 0;
at += (size_t)written;
size -= (size_t)written;
out->written += written;
}
return 1;
}
int holy_spool_open(struct holy_payload *out, struct holy_spool_writer *writer)
{
EVP_MD_CTX *context = EVP_MD_CTX_new();
if (!context) return 0;
if (EVP_DigestInit_ex(context, EVP_sha256(), NULL) != 1) {
EVP_MD_CTX_free(context);
return 0;
}
writer->context = context;
writer->offset = out->written;
writer->size = 0;
return 1;
}
int holy_spool_append(struct holy_payload *out, struct holy_spool_writer *writer,
const void *data, size_t length)
{
if (!writer->context) return 0;
if (length && EVP_DigestUpdate(writer->context, data, length) != 1) return 0;
if (!spool_bytes(out, data, length, &writer->offset)) return 0;
writer->size += (long long)length;
return 1;
}
int holy_spool_close(struct holy_payload *out, struct holy_spool_writer *writer,
unsigned char digest[32])
{
unsigned char whole[32];
unsigned length = 0;
int ok = 0;
(void)out;
if (!writer->context) return 0;
if (EVP_DigestFinal_ex(writer->context, whole, &length) != 1 || length != sizeof whole)
goto done;
memcpy(digest, whole, sizeof whole);
ok = 1;
done:
EVP_MD_CTX_free(writer->context);
writer->context = NULL;
return ok;
}
int holy_payload_spool_bytes(struct holy_payload *out, const void *data, size_t length,
long long *offset, long long *size, unsigned char digest[32])
{
struct holy_spool_writer writer;
if (!holy_spool_open(out, &writer)) return 0;
if (!holy_spool_append(out, &writer, data, length)) return 0;
if (!holy_spool_close(out, &writer, digest)) return 0;
*offset = writer.offset;
*size = writer.size;
return 1;
}
int holy_payload_add_text(struct holy_payload *out, struct holy_text *body, const char *path,
unsigned mode)
{
unsigned char digest[32];
unsigned length = 0;
long long offset;
if (EVP_Digest(body->data, body->used, digest, &length, EVP_sha256(), NULL) != 1 ||
length != 32) return 0;
if (!spool_bytes(out, body->data, body->used, &offset)) return 0;
if (!holy_payload_add(out, path, NULL, mode, offset, (long long)body->used, 0)) return 0;
memcpy(out->digests[out->count - 1], digest, sizeof digest);
return 1;
}
char *holy_payload_link_target(const char *path, const char *target)
{
struct holy_text out = {0};
size_t length = strlen(path) + strlen(target) + 2;
char *joined = malloc(length);
const char *p;
size_t ends[256], count = 0;
if (!joined) return NULL;
if (target[0] == '/') snprintf(joined, length, "%s", target);
else snprintf(joined, length, "%s/%s", path, target);
for (p = joined; *p;) {
const char *end = strchr(p, '/');
size_t part = end ? (size_t)(end - p) : strlen(p);
char piece[4096];
if (part == 1 && p[0] == '.') {
/* an empty component changes nothing */
} else if (part == 2 && p[0] == '.' && p[1] == '.') {
if (count) out.used = ends[--count];
} else if (part) {
if (part >= sizeof piece || count == sizeof ends / sizeof *ends ||
!holy_text_add(&out, "/")) {
free(joined);
holy_text_free(&out);
return NULL;
}
memcpy(piece, p, part);
piece[part] = 0;
if (!holy_text_add(&out, piece)) {
free(joined);
holy_text_free(&out);
return NULL;
}
ends[count++] = out.used;
}
if (!end) break;
p = end + 1;
}
free(joined);
if (!out.used && !holy_text_add(&out, "/")) { holy_text_free(&out); return NULL; }
return out.data;
}
/* one file of the image: its bytes, its ELF facts and what they require */
static int collect_file(struct holy_payload *out, int parent, const char *name,
const char *path)
{
struct holy_elf_info elf;
struct stat st;
char link[4096];
unsigned char digest[32];
long long offset, size;
int fd, parsed;
/* the node is read through the directory, so a link in the image is carried
as a link and never opened */
if (fstatat(parent, name, &st, AT_SYMLINK_NOFOLLOW)) return 0;
if (S_ISLNK(st.st_mode)) {
ssize_t target = readlinkat(parent, name, link, sizeof link - 1);
char *absolute;
if (target < 0) return 0;
link[target] = 0;
++out->links;
if (link[0] != '/' && holy_safe_link(path + 5, link))
return holy_payload_add(out, path, link, 0777, 0, 0, 0);
/* a payload carries no absolute or escaping link, because neither the
extractor nor the installer accepts one. the path the link named is
recorded as a requirement, so nothing is dropped without a trace. */
absolute = holy_payload_link_target(path + 5, link);
++out->path_views;
if (!absolute || !holy_names_add(&out->absolute, absolute)) {
free(absolute);
return 0;
}
free(absolute);
return 1;
}
if (!S_ISREG(st.st_mode)) {
++out->unknown;
return holy_payload_add(out, path, NULL, 0000, 0, 0, 0);
}
fd = openat(parent, name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) return 0;
if (fstat(fd, &st)) { close(fd); return 0; }
if (!holy_payload_spool_file(out, fd, &offset, &size, digest)) { close(fd); return 0; }
if (!holy_payload_add(out, path, NULL, st.st_mode & 07777, offset, size, 0)) {
close(fd);
return 0;
}
memcpy(out->digests[out->count - 1], digest, sizeof digest);
++out->files;
parsed = holy_elf_read_fd(fd, &elf);
if (!parsed) {
const char *machine = holy_elf_machine(&elf), *runtime = holy_elf_runtime(&elf);
size_t i;
if (!strcmp(machine, "unknown") || !strcmp(runtime, "unknown")) {
++out->unknown;
} else {
const char *known_arch = !strcmp(machine, "x86") ? "x86" : "x86_64";
const char *known_libc = !strcmp(runtime, "glibc") || !strcmp(runtime, "musl") ?
runtime : HOLY_PAYLOAD_NOLIBC;
if (!out->arch[0]) {
snprintf(out->arch, sizeof out->arch, "%s", known_arch);
snprintf(out->libc, sizeof out->libc, "%s", known_libc);
} else if (strcmp(out->arch, known_arch) || strcmp(out->libc, known_libc)) {
out->mixed = 1;
}
}
for (i = 0; i < elf.needed_count; ++i)
if (elf.needed[i][0] && !strchr(elf.needed[i], '/') &&
!holy_names_add(&out->needed, elf.needed[i])) {
holy_elf_free(&elf);
close(fd);
return 0;
}
if (elf.soname && !holy_names_add(&out->provided, elf.soname)) {
holy_elf_free(&elf);
close(fd);
return 0;
}
++out->elfs;
holy_elf_free(&elf);
} else {
unsigned char head[2] = {0, 0};
if (pread(fd, head, sizeof head, 0) == (ssize_t)sizeof head &&
head[0] == '#' && head[1] == '!') ++out->scripts;
else if (st.st_mode & 0111 || parsed == 2) ++out->unknown;
}
close(fd);
return 1;
}
int holy_payload_walk(struct holy_payload *out, int parent, const char *prefix,
const char *payload_prefix, unsigned depth)
{
DIR *dir;
struct dirent *entry;
int copy, ok = 1;
if (depth > 64 || out->files + out->links > 100000) return 0;
copy = holy_image_directory(parent);
if (copy < 0) return 0;
dir = fdopendir(copy);
if (!dir) { close(copy); return 0; }
errno = 0;
while ((entry = readdir(dir))) {
struct stat st;
char *path, *placed;
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
path = holy_image_path(prefix, entry->d_name);
placed = path ? holy_image_path(payload_prefix, entry->d_name) : NULL;
if (!path || !placed || fstatat(parent, entry->d_name, &st, AT_SYMLINK_NOFOLLOW)) {
free(path); free(placed); ok = 0; break;
}
if (S_ISDIR(st.st_mode)) {
int child = openat(parent, entry->d_name,
O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (child < 0 || !holy_payload_add(out, placed, NULL, 0755, 0, 0, 1) ||
!holy_payload_walk(out, child, path, placed, depth + 1)) ok = 0;
if (child >= 0) close(child);
} else if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) {
if (!collect_file(out, parent, entry->d_name, placed)) ok = 0;
} else {
/* a device, a socket or a fifo has no place in a package payload */
++out->unknown;
}
free(path);
free(placed);
if (!ok || out->files + out->links > 100000) { ok = 0; break; }
errno = 0;
}
if (errno) ok = 0;
closedir(dir);
return ok;
}
int holy_payload_manifest(FILE *manifest, const struct holy_payload *out, size_t first)
{
size_t i, at;
for (i = first; i < out->count; ++i) {
const struct holy_stream_entry *e = &out->entries[i];
if (strncmp(e->path, "DATA/", 5)) continue;
fputs(e->directory ? "dir " : e->link ? "symlink " : "file ", manifest);
holy_quoted(manifest, e->path + 5);
fprintf(manifest, " %o - - %lld %lld %lld ", e->mode, e->uid, e->gid,
e->directory || e->link ? 0LL : e->size);
if (e->directory || e->link) fputc('-', manifest);
else for (at = 0; at < 32; ++at) fprintf(manifest, "%02x", (unsigned)out->digests[i][at]);
/* kind, hardlink group and link group; only a link carries a target */
fputs(" none - -", manifest);
if (e->link) { fputc(' ', manifest); holy_quoted(manifest, e->link); }
fputc('\n', manifest);
}
return !ferror(manifest);
}
int holy_payload_records(struct holy_payload *payload, FILE *files[7], char *text[7],
size_t sizes[7])
{
static const char *const names[] = {
"HOLY/meta", "HOLY/files", "HOLY/deps", "HOLY/provides", "HOLY/hooks",
"HOLY/origin", "HOLY/transform"
};
size_t i;
for (i = 0; i < 7; ++i) {
const char *body;
size_t left;
/* the memstream sizes settle when it is flushed */
if (fflush(files[i])) return 0;
body = text[i];
left = sizes[i];
if (!holy_payload_add(payload, names[i], NULL, 0644, (long long)payload->written,
(long long)left, 0)) return 0;
while (left) {
ssize_t written = write(payload->spool, body, left);
if (written < 0 && errno == EINTR) continue;
if (written <= 0) return 0;
body += (size_t)written;
left -= (size_t)written;
payload->written += written;
}
if (fclose(files[i])) { files[i] = NULL; return 0; }
files[i] = NULL;
}
return 1;
}