Initial commit: OCI image mounting via containers-storage

slocker-lite validates an OCI Image Layout tar, imports its layers into
containers-storage's layer store in order, and mounts the assembled image
using fuse-overlayfs, printing the resulting merged path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 08:12:50 +00:00
commit a301dfb44f
16 changed files with 1141 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
// Copyright (C) 2026 Viorel Munteanu
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#include "containers_storage.h"
#include "process.h"
namespace {
std::string trim(const std::string& s) {
size_t begin = s.find_first_not_of(" \t\r\n");
if (begin == std::string::npos) {
return "";
}
size_t end = s.find_last_not_of(" \t\r\n");
return s.substr(begin, end - begin + 1);
}
} // namespace
std::string kMountProgram;
std::optional<std::string> import_layer(const std::filesystem::path& diff_file,
const std::string& parent_id) {
std::vector<std::string> argv = {
"containers-storage", "import-layer", "--file", diff_file.string(),
"--storage-opt", "overlay.mount_program=" + kMountProgram};
if (!parent_id.empty()) {
argv.push_back(parent_id);
}
ProcessResult result = run_process(argv);
std::string id = trim(result.stdout_output);
if (result.exit_code != 0 || id.empty()) {
return std::nullopt;
}
return id;
}
std::optional<std::string> mount_layer(const std::string& layer_id) {
std::vector<std::string> argv = {
"containers-storage", "mount", "--storage-opt",
"overlay.mount_program=" + kMountProgram, layer_id};
ProcessResult result = run_process(argv);
std::string path = trim(result.stdout_output);
if (result.exit_code != 0 || path.empty()) {
return std::nullopt;
}
return path;
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright (C) 2026 Viorel Munteanu
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#pragma once
#include <filesystem>
#include <optional>
#include <string>
// Absolute path to the fuse-overlayfs binary, passed to containers-storage as
// overlay.mount_program so it performs the actual overlay mounting. Set once in main()
// after find_in_path() resolves it.
extern std::string kMountProgram;
// Runs `containers-storage import-layer --storage-opt overlay.mount_program=<path>
// [--file diff_file] [parent_id]`. Returns the new layer's ID (trimmed stdout) or
// nullopt on failure (containers-storage's own stderr is inherited/visible).
std::optional<std::string> import_layer(const std::filesystem::path& diff_file,
const std::string& parent_id);
// Runs `containers-storage mount --storage-opt overlay.mount_program=<path> <layer_id>`.
// Returns the merged directory path (trimmed stdout) or nullopt on failure.
std::optional<std::string> mount_layer(const std::string& layer_id);
+128
View File
@@ -0,0 +1,128 @@
// Copyright (C) 2026 Viorel Munteanu
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#include <array>
#include <cstdlib>
#include <filesystem>
#include <optional>
#include <string_view>
#include <unistd.h>
#include <fmt/core.h>
#include <spdlog/cfg/env.h>
#include <spdlog/spdlog.h>
#include "containers_storage.h"
#include "oci_image.h"
namespace {
constexpr std::array<std::string_view, 2> kRequiredTools = {"containers-storage", "bwrap"};
bool is_executable_file(const std::filesystem::path& path) {
std::error_code ec;
return std::filesystem::is_regular_file(path, ec) && access(path.c_str(), X_OK) == 0;
}
std::optional<std::filesystem::path> find_in_path(std::string_view name) {
const char* path_env = std::getenv("PATH");
if (!path_env) {
return std::nullopt;
}
std::string_view path{path_env};
for (size_t start = 0; start <= path.size();) {
size_t end = path.find(':', start);
if (end == std::string_view::npos) {
end = path.size();
}
std::filesystem::path dir{path.substr(start, end - start)};
if (!dir.empty()) {
std::filesystem::path candidate = dir / name;
if (is_executable_file(candidate)) {
return candidate;
}
}
start = end + 1;
}
return std::nullopt;
}
} // namespace
bool check_required_dependencies() {
bool all_found = true;
for (auto name : kRequiredTools) {
if (!find_in_path(name)) {
spdlog::error("required dependency not found in PATH: {}", name);
all_found = false;
}
}
return all_found;
}
int main(int argc, char* argv[]) {
spdlog::cfg::load_env_levels();
if (argc != 2) {
spdlog::error("usage: {} <image.tar>", argv[0]);
return 1;
}
const std::filesystem::path image_tar = argv[1];
if (!check_required_dependencies()) {
return 1;
}
auto mount_program = find_in_path("fuse-overlayfs");
if (!mount_program) {
spdlog::error("fuse-overlayfs not found in PATH");
return 1;
}
kMountProgram = mount_program->string();
auto layers = read_oci_layers(image_tar);
if (!layers) {
return 1;
}
std::string parent_id;
for (const auto& layer : *layers) {
std::filesystem::path tmp_file =
std::filesystem::temp_directory_path() /
fmt::format("slocker-lite-{}-{}", getpid(), oci_digest_hex(layer.digest));
if (!extract_blob_to_file(image_tar, oci_digest_hex(layer.digest), tmp_file)) {
return 1;
}
auto layer_id = import_layer(tmp_file, parent_id);
std::filesystem::remove(tmp_file);
if (!layer_id) {
spdlog::error("failed to import layer {}", layer.digest);
return 1;
}
parent_id = *layer_id;
}
auto merged = mount_layer(parent_id);
if (!merged) {
spdlog::error("failed to mount assembled image");
return 1;
}
fmt::print("mounted image at: {}\n", *merged);
return 0;
}
+195
View File
@@ -0,0 +1,195 @@
// Copyright (C) 2026 Viorel Munteanu
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#include "oci_image.h"
#include <archive.h>
#include <archive_entry.h>
#include <fmt/core.h>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <fstream>
#include <functional>
namespace {
using json = nlohmann::json;
std::string_view normalize_entry_path(std::string_view name) {
if (name.substr(0, 2) == "./") {
name.remove_prefix(2);
}
return name;
}
// Reads a single tar entry's content, calling `sink` with each chunk of data as it's
// read. `sink` is only invoked for the entry whose normalized path equals `target_name`.
// Returns true if the entry was found (regardless of whether it was empty).
bool for_each_chunk_in_entry(const std::filesystem::path& tar_path, std::string_view target_name,
const std::function<void(const char*, size_t)>& sink) {
struct archive* a = archive_read_new();
archive_read_support_filter_all(a);
archive_read_support_format_tar(a);
if (archive_read_open_filename(a, tar_path.c_str(), 65536) != ARCHIVE_OK) {
archive_read_free(a);
return false;
}
bool found = false;
struct archive_entry* entry;
while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
if (normalize_entry_path(archive_entry_pathname(entry)) != target_name) {
archive_read_data_skip(a);
continue;
}
found = true;
const void* buff;
size_t size;
int64_t offset;
while (true) {
int rc = archive_read_data_block(a, &buff, &size, &offset);
if (rc == ARCHIVE_EOF) {
break;
}
if (rc != ARCHIVE_OK) {
found = false;
break;
}
sink(static_cast<const char*>(buff), size);
}
break;
}
archive_read_free(a);
return found;
}
std::optional<std::string> read_entry_to_string(const std::filesystem::path& tar_path,
std::string_view target_name) {
std::string content;
bool found = for_each_chunk_in_entry(tar_path, target_name, [&](const char* data, size_t size) {
content.append(data, size);
});
if (!found) {
return std::nullopt;
}
return content;
}
} // namespace
std::string oci_digest_hex(std::string_view digest) {
constexpr std::string_view prefix = "sha256:";
if (digest.substr(0, prefix.size()) == prefix) {
digest.remove_prefix(prefix.size());
}
return std::string(digest);
}
bool extract_blob_to_file(const std::filesystem::path& tar_path, std::string_view digest_hex,
const std::filesystem::path& out_file) {
std::string entry_name = fmt::format("blobs/sha256/{}", digest_hex);
std::ofstream out(out_file, std::ios::binary | std::ios::trunc);
if (!out) {
spdlog::error("failed to open {} for writing", out_file.string());
return false;
}
bool found = for_each_chunk_in_entry(tar_path, entry_name,
[&](const char* data, size_t size) { out.write(data, static_cast<std::streamsize>(size)); });
if (!found) {
spdlog::error("blob {} not found in {}", entry_name, tar_path.string());
return false;
}
return true;
}
std::optional<std::vector<OciLayer>> read_oci_layers(const std::filesystem::path& tar_path) {
auto layout = read_entry_to_string(tar_path, "oci-layout");
if (!layout) {
spdlog::error("not an OCI image tar: missing oci-layout");
return std::nullopt;
}
try {
[[maybe_unused]] json parsed_layout = json::parse(*layout);
} catch (const json::parse_error& e) {
spdlog::error("oci-layout is not valid JSON: {}", e.what());
return std::nullopt;
}
auto index_content = read_entry_to_string(tar_path, "index.json");
if (!index_content) {
spdlog::error("not an OCI image tar: missing index.json");
return std::nullopt;
}
json index;
try {
index = json::parse(*index_content);
} catch (const json::parse_error& e) {
spdlog::error("index.json is not valid JSON: {}", e.what());
return std::nullopt;
}
std::string manifest_digest;
for (const auto& m : index.value("manifests", json::array())) {
if (m.value("mediaType", "") == "application/vnd.oci.image.manifest.v1+json") {
manifest_digest = m.value("digest", "");
break;
}
}
if (manifest_digest.empty()) {
spdlog::error("index.json has no OCI image manifest entry");
return std::nullopt;
}
std::string manifest_entry = fmt::format("blobs/sha256/{}", oci_digest_hex(manifest_digest));
auto manifest_content = read_entry_to_string(tar_path, manifest_entry);
if (!manifest_content) {
spdlog::error("missing image manifest blob {}", manifest_digest);
return std::nullopt;
}
json manifest;
try {
manifest = json::parse(*manifest_content);
} catch (const json::parse_error& e) {
spdlog::error("image manifest {} is not valid JSON: {}", manifest_digest, e.what());
return std::nullopt;
}
std::vector<OciLayer> layers;
for (const auto& l : manifest.value("layers", json::array())) {
OciLayer layer;
layer.digest = l.value("digest", "");
layer.media_type = l.value("mediaType", "");
if (layer.digest.empty()) {
spdlog::error("image manifest {} has a layer with no digest", manifest_digest);
return std::nullopt;
}
layers.push_back(std::move(layer));
}
if (layers.empty()) {
spdlog::error("image manifest {} has no layers", manifest_digest);
return std::nullopt;
}
return layers;
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright (C) 2026 Viorel Munteanu
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
struct OciLayer {
std::string digest; // "sha256:<hex>", as it appears in the manifest
std::string media_type;
};
// Validates that `tar_path` is an OCI Image Layout tar (oci-layout + index.json +
// a referenced blobs/sha256/<digest> image manifest) and returns its layers in
// base-to-top order. Logs a specific error via fmt and returns nullopt if anything
// required is missing or malformed.
std::optional<std::vector<OciLayer>> read_oci_layers(const std::filesystem::path& tar_path);
// Extracts the raw bytes of blobs/sha256/<digest_hex> from `tar_path` into `out_file`.
// Returns false (and logs) if the entry isn't found.
bool extract_blob_to_file(const std::filesystem::path& tar_path,
std::string_view digest_hex,
const std::filesystem::path& out_file);
// Strips the "sha256:" algorithm prefix from a digest string, e.g.
// "sha256:abcd" -> "abcd". Returns the input unchanged if there is no such prefix.
std::string oci_digest_hex(std::string_view digest);
+78
View File
@@ -0,0 +1,78 @@
// Copyright (C) 2026 Viorel Munteanu
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#include "process.h"
#include <sys/wait.h>
#include <unistd.h>
#include <cstring>
#include <fmt/ranges.h>
#include <spdlog/spdlog.h>
ProcessResult run_process(const std::vector<std::string>& argv) {
spdlog::debug("running external command: {}", fmt::join(argv, " "));
int stdout_pipe[2];
if (pipe(stdout_pipe) != 0) {
return {-1, ""};
}
pid_t pid = fork();
if (pid < 0) {
close(stdout_pipe[0]);
close(stdout_pipe[1]);
return {-1, ""};
}
if (pid == 0) {
close(stdout_pipe[0]);
dup2(stdout_pipe[1], STDOUT_FILENO);
close(stdout_pipe[1]);
std::vector<char*> c_argv;
c_argv.reserve(argv.size() + 1);
for (const auto& arg : argv) {
c_argv.push_back(const_cast<char*>(arg.c_str()));
}
c_argv.push_back(nullptr);
execvp(c_argv[0], c_argv.data());
const char* msg = "run_process: execvp failed\n";
write(STDERR_FILENO, msg, std::strlen(msg));
_exit(127);
}
close(stdout_pipe[1]);
std::string output;
char buffer[4096];
ssize_t n;
while ((n = read(stdout_pipe[0], buffer, sizeof(buffer))) > 0) {
output.append(buffer, static_cast<size_t>(n));
}
close(stdout_pipe[0]);
int status = 0;
waitpid(pid, &status, 0);
int exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
if (exit_code != 0) {
spdlog::warn("external command failed (exit code {}): {}", exit_code, fmt::join(argv, " "));
}
return {exit_code, output};
}
+29
View File
@@ -0,0 +1,29 @@
// Copyright (C) 2026 Viorel Munteanu
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#pragma once
#include <string>
#include <vector>
struct ProcessResult {
int exit_code;
std::string stdout_output;
};
// Runs argv[0] with the given arguments via fork/execvp, capturing stdout.
// stderr is inherited so the child's own error messages reach the user directly.
ProcessResult run_process(const std::vector<std::string>& argv);