|
| 1 | +/* SPDX-License-Identifier: GPL-2.0-or-later |
| 2 | + * Lazy private-inode materialization. Never hand a donor FD to a tracee. |
| 3 | + * PRoot's single event loop completes materialization before resuming a guest. |
| 4 | + * Inode keys preserve hardlink/rename identity. No per-file descriptor inventory. |
| 5 | + */ |
| 6 | +#include <errno.h> |
| 7 | +#include <fcntl.h> |
| 8 | +#include <stdint.h> |
| 9 | +#include <stdio.h> |
| 10 | +#include <stdlib.h> |
| 11 | +#include <string.h> |
| 12 | +#include <sys/ioctl.h> |
| 13 | +#include <sys/stat.h> |
| 14 | +#include <unistd.h> |
| 15 | +#include <uthash.h> |
| 16 | +#include "extension/extension.h" |
| 17 | +#include "tracee/reg.h" |
| 18 | +#include "syscall/sysnum.h" |
| 19 | + |
| 20 | +#define BMS_FICLONE 0x40049409 |
| 21 | + |
| 22 | +typedef struct { uint64_t dev, ino; } Key; |
| 23 | +typedef struct CowFile { |
| 24 | + Key key; |
| 25 | + struct stat donor_stat; |
| 26 | + char *lower; |
| 27 | + int copied; |
| 28 | + UT_hash_handle hh; |
| 29 | +} CowFile; |
| 30 | +static CowFile *files; |
| 31 | + |
| 32 | +static Key key_of(const struct stat *s) { |
| 33 | + Key k = {s->st_dev, s->st_ino}; |
| 34 | + return k; |
| 35 | +} |
| 36 | +static int unchanged(const struct stat *a, const struct stat *b) { |
| 37 | + return a->st_dev == b->st_dev && a->st_ino == b->st_ino && |
| 38 | + a->st_mode == b->st_mode && a->st_nlink == b->st_nlink && |
| 39 | + a->st_size == b->st_size && |
| 40 | + a->st_mtim.tv_sec == b->st_mtim.tv_sec && a->st_mtim.tv_nsec == b->st_mtim.tv_nsec && |
| 41 | + a->st_ctim.tv_sec == b->st_ctim.tv_sec && a->st_ctim.tv_nsec == b->st_ctim.tv_nsec; |
| 42 | +} |
| 43 | +static int copy_up(CowFile *f, const char *upper_path) { |
| 44 | + int src = -1, dst = -1, result = 0; |
| 45 | + struct stat before, after, upper; |
| 46 | + struct timespec times[2]; |
| 47 | + char data[131072]; |
| 48 | + off_t offset = 0; |
| 49 | + ssize_t n; |
| 50 | + if (f->copied) return 0; |
| 51 | + src = open(f->lower, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); |
| 52 | + if (src < 0) return -errno; |
| 53 | + if (fstat(src, &before) || !unchanged(&before, &f->donor_stat)) { |
| 54 | + result = -ESTALE; goto out; |
| 55 | + } |
| 56 | + if (stat(upper_path, &upper)) { result = -errno; goto out; } |
| 57 | + if (chmod(upper_path, (upper.st_mode & 07777) | 0600)) { result = -errno; goto out; } |
| 58 | + dst = open(upper_path, O_WRONLY | O_CLOEXEC); |
| 59 | + if (dst < 0) { result = -errno; chmod(upper_path, upper.st_mode & 07777); goto out; } |
| 60 | + if (ioctl(dst, BMS_FICLONE, src)) { |
| 61 | + if (errno != EXDEV && errno != EOPNOTSUPP && errno != ENOTTY && |
| 62 | + errno != EINVAL && errno != ENOSYS) { result = -errno; goto restore; } |
| 63 | + while (offset < before.st_size) { |
| 64 | + n = pread(src, data, sizeof(data), offset); |
| 65 | + if (n < 0 && errno == EINTR) continue; |
| 66 | + if (n <= 0) { result = -(n < 0 ? errno : EIO); goto restore; } |
| 67 | + ssize_t done = 0; |
| 68 | + while (done < n) { |
| 69 | + ssize_t wrote = pwrite(dst, data + done, n - done, offset + done); |
| 70 | + if (wrote < 0 && errno == EINTR) continue; |
| 71 | + if (wrote <= 0) { result = -(wrote < 0 ? errno : EIO); goto restore; } |
| 72 | + done += wrote; |
| 73 | + } |
| 74 | + offset += n; |
| 75 | + } |
| 76 | + if (ftruncate(dst, before.st_size)) { result = -errno; goto restore; } |
| 77 | + } |
| 78 | + if (fstat(src, &after) || !unchanged(&before, &after)) { result = -ESTALE; goto restore; } |
| 79 | + f->copied = 1; |
| 80 | +restore: |
| 81 | + times[0] = upper.st_atim; times[1] = upper.st_mtim; |
| 82 | + if (futimens(dst, times) && !result) result = -errno; |
| 83 | + if (fchmod(dst, upper.st_mode & 07777) && !result) result = -errno; |
| 84 | +out: |
| 85 | + if (dst >= 0) close(dst); |
| 86 | + if (src >= 0) close(src); |
| 87 | + return result; |
| 88 | +} |
| 89 | +static int load_map(const char *path) { |
| 90 | + FILE *stream = fopen(path, "rb"); |
| 91 | + char *upper = NULL, *lower = NULL; |
| 92 | + size_t a = 0, b = 0; |
| 93 | + int result = 0; |
| 94 | + if (!stream) return -errno; |
| 95 | + while (getdelim(&upper, &a, 0, stream) >= 0) { |
| 96 | + struct stat s; |
| 97 | + CowFile *f; |
| 98 | + if (getdelim(&lower, &b, 0, stream) < 0 || upper[0] != '/' || lower[0] != '/') { |
| 99 | + result = -EINVAL; break; |
| 100 | + } |
| 101 | + f = calloc(1, sizeof(*f)); |
| 102 | + if (!f) { result = -ENOMEM; break; } |
| 103 | + if (lstat(upper, &s) || !S_ISREG(s.st_mode) || lstat(lower, &f->donor_stat) || |
| 104 | + !S_ISREG(f->donor_stat.st_mode) || s.st_size != f->donor_stat.st_size || |
| 105 | + (s.st_dev == f->donor_stat.st_dev && s.st_ino == f->donor_stat.st_ino)) { |
| 106 | + free(f); result = -EINVAL; break; |
| 107 | + } |
| 108 | + f->key = key_of(&s); |
| 109 | + f->lower = strdup(lower); |
| 110 | + HASH_ADD(hh, files, key, sizeof(f->key), f); |
| 111 | + } |
| 112 | + if (ferror(stream) && !result) result = -EIO; |
| 113 | + free(upper); free(lower); fclose(stream); |
| 114 | + return result; |
| 115 | +} |
| 116 | +int cow_callback(Extension *extension, ExtensionEvent event, intptr_t data1, intptr_t data2) { |
| 117 | + (void)extension; (void)data2; |
| 118 | + switch (event) { |
| 119 | + case INITIALIZATION: return load_map((const char *)data1); |
| 120 | + case INHERIT_PARENT: return 1; |
| 121 | + case TRANSLATED_PATH: { |
| 122 | + struct stat s; |
| 123 | + CowFile *f; |
| 124 | + Key key; |
| 125 | + if (stat((char *)data1, &s) || !S_ISREG(s.st_mode)) return 0; |
| 126 | + key = key_of(&s); |
| 127 | + HASH_FIND(hh, files, &key, sizeof(key), f); |
| 128 | + if (!f || f->copied) return 0; |
| 129 | + /* Stat/access alone need no content. All descriptor-producing paths, |
| 130 | + * including O_PATH, and PRoot's ELF/shebang translations come here. */ |
| 131 | + switch (get_sysnum(TRACEE(extension), ORIGINAL)) { |
| 132 | + case PR_stat: case PR_lstat: case PR_stat64: case PR_lstat64: |
| 133 | + case PR_newfstatat: case PR_statx: case PR_access: case PR_faccessat: |
| 134 | + case PR_readlink: case PR_readlinkat: return 0; |
| 135 | + default: return copy_up(f, (const char *)data1); |
| 136 | + } |
| 137 | + } |
| 138 | + default: return 0; |
| 139 | + } |
| 140 | +} |
0 commit comments