Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
pack_install.hpp
Go to the documentation of this file.
1#pragma once
2
3// Content-addressed, immutable install for a verified swap pack. The trust gate
4// verifies bytes on disk, but if the loader later re-reads those paths a local
5// attacker (or a buggy installer) can swap the file between verify and load — a
6// time-of-check/time-of-use hole that affects the re-read script/theme/asset paths,
7// not just native dlopen. The fix: copy the verified files ONCE into a read-only
8// directory named by the pack's content hash, re-verify the staged bytes, and have
9// the loader read only from there. The installed bytes are the verified bytes.
10//
11// Control thread only (reads/writes files). Reuses verify_swap_pack (the sole
12// sanctioned trust entry) — never the integrity check alone.
13
16
17#include <cstdint>
18#include <filesystem>
19#include <fstream>
20#include <string>
21#include <system_error>
22#include <vector>
23
24namespace pulp::format::reload {
25
33
36 std::string detail;
37 std::filesystem::path installed_root;
38 bool ok() const { return status == PackInstall::Ok; }
39};
40
45inline std::string swap_pack_content_id(const SwapPackManifest& m) {
46 const auto msg = swap_pack_signed_message(m);
47 return runtime::sha256_hex(msg.data(), msg.size());
48}
49
57 const std::filesystem::path& source_root, const SwapPackManifest& manifest,
58 const std::vector<std::uint8_t>& trusted_public_key,
59 const std::filesystem::path& install_base, bool reject_unknown_kinds = true) {
60 namespace fs = std::filesystem;
61
62 // 1. The pack must be trustworthy at the source before we copy anything.
63 const auto v = verify_swap_pack(source_root, manifest, trusted_public_key);
64 if (!v.ok()) return {PackInstall::NotTrusted, v.detail, {}};
65
66 // 2. Reject symlinks and path escapes up front. A declared file that is a
67 // symlink (or resolves outside the pack root) could redirect the copy to
68 // bytes that were never hashed.
69 std::error_code ec;
70 const fs::path canon_root = fs::weakly_canonical(source_root, ec);
71 for (const auto& f : manifest.files) {
72 if (reject_unknown_kinds && f.kind == SwapPackKind::Unknown)
73 return {PackInstall::UnknownKind, f.path, {}};
74 const fs::path src = source_root / f.path;
75 if (fs::is_symlink(src, ec) || ec)
76 return {PackInstall::SymlinkRejected, f.path, {}};
77 const fs::path canon = fs::weakly_canonical(src, ec);
78 if (ec || (!canon_root.empty() &&
79 canon.string().rfind(canon_root.string(), 0) != 0))
80 return {PackInstall::SymlinkRejected, f.path, {}};
81 }
82
83 const fs::path dest = install_base / swap_pack_content_id(manifest);
84
85 // 3. Already installed → re-verify the immutable copy and reuse it. If a prior
86 // install was corrupted, fall through and re-stage over it.
87 if (fs::exists(dest, ec)) {
88 if (verify_swap_pack(dest, manifest, trusted_public_key).ok())
89 return {PackInstall::Ok, "", dest};
90 fs::permissions(dest, fs::perms::owner_all, fs::perm_options::add, ec);
91 fs::remove_all(dest, ec);
92 }
93
94 // 4. Stage into a sibling temp dir, copy each verified file byte-for-byte
95 // (symlinks already excluded, so a plain read is safe), then re-verify the
96 // STAGED bytes before publishing — the installed bytes are the verified bytes.
97 fs::create_directories(install_base, ec);
98 const fs::path staging = install_base / (".staging-" + swap_pack_content_id(manifest));
99 fs::remove_all(staging, ec);
100 if (!fs::create_directories(staging, ec) && ec)
101 return {PackInstall::IoError, "create staging dir: " + ec.message(), {}};
102
103 auto fail_io = [&](std::string what) -> PackInstallResult {
104 fs::permissions(staging, fs::perms::owner_all, fs::perm_options::add, ec);
105 fs::remove_all(staging, ec);
106 return {PackInstall::IoError, std::move(what), {}};
107 };
108
109 for (const auto& f : manifest.files) {
110 const fs::path src = source_root / f.path;
111 const fs::path out = staging / f.path;
112 fs::create_directories(out.parent_path(), ec);
113 std::ifstream in(src, std::ios::binary);
114 if (!in) return fail_io("read " + f.path);
115 std::vector<char> bytes((std::istreambuf_iterator<char>(in)),
116 std::istreambuf_iterator<char>());
117 std::ofstream o(out, std::ios::binary | std::ios::trunc);
118 if (!o) return fail_io("write " + f.path);
119 o.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
120 o.close();
121 if (!o) return fail_io("flush " + f.path);
122 // Read-only file (owner read). The dir is locked down after the rename.
123 fs::permissions(out, fs::perms::owner_read, fs::perm_options::replace, ec);
124 }
125
126 // 5. Re-verify the staged copy: the bytes we will publish must themselves match
127 // the signed hashes (guards a copy that silently corrupted or truncated).
128 if (!verify_swap_pack(staging, manifest, trusted_public_key).ok())
129 return fail_io("staged bytes failed re-verification");
130
131 // 6. Publish atomically. If a concurrent installer won the race, its dir is the
132 // same content id (byte-identical), so either result is correct.
133 fs::rename(staging, dest, ec);
134 if (ec) {
135 if (fs::exists(dest) &&
136 verify_swap_pack(dest, manifest, trusted_public_key).ok()) {
137 fs::permissions(staging, fs::perms::owner_all, fs::perm_options::add, ec);
138 fs::remove_all(staging, ec);
139 return {PackInstall::Ok, "", dest};
140 }
141 return fail_io("publish rename: " + ec.message());
142 }
143 // Lock the published dir read+exec only (owner). Files are already read-only.
144 fs::permissions(dest, fs::perms::owner_read | fs::perms::owner_exec,
145 fs::perm_options::replace, ec);
146 return {PackInstall::Ok, "", dest};
147}
148
149} // namespace pulp::format::reload
Definition build_fingerprint.hpp:23
SwapPackVerifyResult verify_swap_pack(const std::filesystem::path &root, const SwapPackManifest &manifest, const std::vector< std::uint8_t > &trusted_public_key)
Full fail-closed verify: signature FIRST (binds the file hashes without reading the files),...
Definition swap_pack.hpp:300
PackInstallResult install_verified_pack(const std::filesystem::path &source_root, const SwapPackManifest &manifest, const std::vector< std::uint8_t > &trusted_public_key, const std::filesystem::path &install_base, bool reject_unknown_kinds=true)
Verify source_root against trusted_public_key, then install the pack's files into install_base/<conte...
Definition pack_install.hpp:56
std::string swap_pack_content_id(const SwapPackManifest &m)
The content id is the SHA-256 of the pack's signed message — every signed field (id,...
Definition pack_install.hpp:45
std::vector< std::uint8_t > swap_pack_signed_message(const SwapPackManifest &m)
Deterministic canonical payload the pack signature covers.
Definition swap_pack.hpp:228
@ Unknown
Definition swap_pack.hpp:37
PackInstall
Definition pack_install.hpp:26
@ SymlinkRejected
a declared file is a symlink or escapes the pack root
Definition pack_install.hpp:29
@ UnknownKind
a declared file has an unrecognized kind (consumer packs)
Definition pack_install.hpp:30
@ NotTrusted
verify_swap_pack failed (bad signature or integrity)
Definition pack_install.hpp:28
@ Ok
Definition pack_install.hpp:27
@ IoError
staging copy / rename / permission set failed
Definition pack_install.hpp:31
@ ok
Definition offline_sample_bounce.hpp:20
std::string sha256_hex(const uint8_t *data, size_t size)
Compute SHA-256 hash as hex string (64 chars).
Definition pack_install.hpp:34
bool ok() const
Definition pack_install.hpp:38
std::string detail
which file / what went wrong
Definition pack_install.hpp:36
PackInstall status
Definition pack_install.hpp:35
std::filesystem::path installed_root
the immutable dir to load from (on Ok)
Definition pack_install.hpp:37
Definition swap_pack.hpp:66
std::vector< SwapPackFile > files
Definition swap_pack.hpp:70
Sealed swap-pack manifest + per-file integrity.