Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 112 additions & 1 deletion libs/linglong/src/linglong/package/layer_packager.cpp
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
/*
* SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd.
* SPDX-FileCopyrightText: 2022 - 2026 UnionTech Software Technology Co., Ltd.
*
* SPDX-License-Identifier: LGPL-3.0-or-later
*/

#include "linglong/package/layer_packager.h"

#include "linglong/api/types/v1/Generators.hpp"

Check warning on line 9 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/api/types/v1/Generators.hpp" not found.

Check warning on line 9 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/api/types/v1/Generators.hpp" not found.
#include "linglong/api/types/v1/LayerInfo.hpp"

Check warning on line 10 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/api/types/v1/LayerInfo.hpp" not found.

Check warning on line 10 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/api/types/v1/LayerInfo.hpp" not found.
#include "linglong/common/error.h"

Check warning on line 11 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/utils/cmd.h" not found.

Check warning on line 11 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/utils/cmd.h" not found.
#include "linglong/utils/cmd.h"

Check warning on line 12 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/utils/file.h" not found.

Check warning on line 12 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/utils/file.h" not found.
#include "linglong/utils/file.h"

Check warning on line 13 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/utils/log/log.h" not found.

Check warning on line 13 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "linglong/utils/log/log.h" not found.
#include "linglong/utils/finally/finally.h"
#include "linglong/utils/log/log.h"

Check warning on line 15 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QDataStream> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 15 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QDataStream> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 16 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QSysInfo> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 16 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QSysInfo> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <nlohmann/json.hpp>

Check warning on line 18 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <filesystem> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 18 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <filesystem> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QDataStream>

Check warning on line 19 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <fstream> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 19 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <fstream> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QSysInfo>

Check warning on line 20 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <string> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 20 in libs/linglong/src/linglong/package/layer_packager.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <string> not found. Please note: Cppcheck does not need standard library headers to get proper results.

#include <array>
#include <filesystem>
#include <fstream>
#include <string>

#include <fcntl.h>
#include <unistd.h>

namespace linglong::package {
Expand Down Expand Up @@ -283,4 +289,109 @@
return utils::Cmd("erofsfuse").exists();
}

utils::error::Result<void> LayerPackager::extractSignData(LayerFile &file,
const std::filesystem::path &signDir)
{
LINGLONG_TRACE("extract sign data from layer");

const auto &number = magicNumber();
file.seek(number.size());

QDataStream metaLenStream(&file);
metaLenStream.setByteOrder(QDataStream::LittleEndian);
quint32 metaLen = 0;
metaLenStream >> metaLen;

auto metaRawData = file.read(metaLen);
auto metaJson = nlohmann::json::parse(metaRawData.toStdString());
auto erofsSize = metaJson.value("erofs_size", 0ULL);
auto signSize = metaJson.value("sign_size", 0ULL);
Comment on lines +305 to +308

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

nlohmann::json::parse throws a parse_error exception if the input is not valid JSON. Since metaRawData comes from an external file on disk, a corrupted or maliciously crafted layer file could crash the daemon.

Use the non-throwing overload of parse by passing false for allow_exceptions, and verify that the parsed JSON is indeed an object before accessing its keys.

Suggested change
auto metaRawData = file.read(metaLen);
auto metaJson = nlohmann::json::parse(metaRawData.toStdString());
auto erofsSize = metaJson.value("erofs_size", 0ULL);
auto signSize = metaJson.value("sign_size", 0ULL);
auto metaRawData = file.read(metaLen);
auto metaJson = nlohmann::json::parse(metaRawData.toStdString(), nullptr, false);
if (metaJson.is_discarded()) {
return LINGLONG_ERR("failed to parse metadata JSON");
}
if (!metaJson.is_object()) {
return LINGLONG_ERR("metadata JSON is not an object");
}
auto erofsSize = metaJson.value("erofs_size", 0ULL);
auto signSize = metaJson.value("sign_size", 0ULL);


if (signSize == 0) {
return LINGLONG_OK;
}

auto signOffset = static_cast<int64_t>(number.size() + sizeof(quint32) + metaLen + erofsSize);

std::error_code ec;
auto destination = signDir / "entries" / "share" / "deepin-elf-verify" / ".elfsign";
if (!std::filesystem::create_directories(destination, ec) && ec) {
return LINGLONG_ERR(ec.message().c_str());
}

auto tarFile = destination / "sign.tar";
auto tarFd = ::open(tarFile.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (tarFd == -1) {
return LINGLONG_ERR(
fmt::format("open {} failed: {}", tarFile.string(), common::error::errorString(errno)));
}

auto removeTar = utils::finally::finally([&tarFd, &tarFile] {
if (tarFd != -1) {
::close(tarFd);
}

std::error_code ec;
if (!std::filesystem::remove(tarFile, ec) && ec) {
LogW("failed to remove {}: {}", tarFile.string(), ec.message());
}
});

file.seek(signOffset);
auto selfFd = file.handle();

auto totalBytes = signSize;
std::array<unsigned char, 4096> buf{};
while (totalBytes > 0) {
auto bytesToRead = totalBytes > buf.size() ? buf.size() : totalBytes;
auto readBytes = ::read(selfFd, buf.data(), bytesToRead);
if (readBytes == -1) {
if (errno == EINTR) {
errno = 0;
continue;
}
return LINGLONG_ERR(
fmt::format("read sign data error: {}", common::error::errorString(errno)));
}
Comment on lines +347 to +355

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

If the layer file is truncated or corrupted such that the actual signature data is smaller than signSize, ::read will return 0 (EOF). In this case, writeBytes will also be 0, writeBytes != readBytes will be false, and totalBytes will not decrease. This results in an infinite loop that consumes 100% CPU.

To fix this, explicitly check for readBytes == 0 and return an error.

        auto readBytes = ::read(selfFd, buf.data(), bytesToRead);
        if (readBytes == 0) {
            return LINGLONG_ERR("unexpected EOF while reading signature data");
        }
        if (readBytes == -1) {
            if (errno == EINTR) {
                errno = 0;
                continue;
            }
            return LINGLONG_ERR(
              fmt::format("read sign data error: {}", common::error::errorString(errno)));
        }


while (true) {
auto writeBytes = ::write(tarFd, buf.data(), readBytes);
if (writeBytes == -1) {
if (errno == EINTR) {
errno = 0;
continue;
}
return LINGLONG_ERR(
fmt::format("write sign.tar error: {}", common::error::errorString(errno)));
}

if (writeBytes != readBytes) {
return LINGLONG_ERR("write sign.tar failed: byte mismatch");
}

totalBytes -= writeBytes;
break;
}
}

if (::fsync(tarFd) == -1) {
return LINGLONG_ERR(
fmt::format("fsync sign.tar error: {}", common::error::errorString(errno)));
}

if (::close(tarFd) == -1) {
tarFd = -1;
return LINGLONG_ERR(
fmt::format("failed to close tar: {}", common::error::errorString(errno)));
}
tarFd = -1;

auto ret = utils::Cmd("tar").exec({ "-xf", tarFile.string(), "-C", destination.string() });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Extracting an untrusted tarball (sign.tar) as root without restricting permissions or ownership poses a security risk. A malicious layer could include setuid binaries or attempt directory traversal.

To mitigate this, pass --no-same-owner and --no-same-permissions to the tar command to prevent extracting files with arbitrary owners or setuid permissions.

Suggested change
auto ret = utils::Cmd("tar").exec({ "-xf", tarFile.string(), "-C", destination.string() });
auto ret = utils::Cmd("tar").exec({ "-xf", tarFile.string(), "--no-same-owner", "--no-same-permissions", "-C", destination.string() });

if (!ret) {
return LINGLONG_ERR(ret);
}

return LINGLONG_OK;
}

} // namespace linglong::package
5 changes: 4 additions & 1 deletion libs/linglong/src/linglong/package/layer_packager.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd.
* SPDX-FileCopyrightText: 2022 - 2026 UnionTech Software Technology Co., Ltd.
*
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
Expand Down Expand Up @@ -38,6 +38,9 @@ class LayerPackager : public QObject
void setCompressor(const QString &compressor) noexcept;
const std::filesystem::path &getWorkDir() const;

utils::error::Result<void> extractSignData(LayerFile &file,
const std::filesystem::path &signDir);

private:
std::filesystem::path workDir;
QString compressor = "lzma";
Expand Down
48 changes: 45 additions & 3 deletions libs/linglong/src/linglong/package_manager/package_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,20 @@ QVariantMap PackageManager::installFromLayer(const QDBusUnixFileDescriptor &fd,
}

taskRef.updateProgress(60);
auto result = this->repo->importLayerDir(*layerDir);
auto signOverlayRootDir = layerPackager.getWorkDir() / "sign";
auto signRet = layerPackager.extractSignData(*layerFile, signOverlayRootDir);
if (!signRet) {
taskRef.reportError(std::move(signRet).error());
return;
}
std::vector<std::filesystem::path> overlays;
auto signDir =
signOverlayRootDir / "entries" / "share" / "deepin-elf-verify" / ".elfsign";
if (std::filesystem::exists(signDir)) {
overlays.emplace_back(signOverlayRootDir);
}
Comment thread
dengbo11 marked this conversation as resolved.

auto result = this->repo->importLayerDir(*layerDir, overlays);
if (!result) {
taskRef.reportError(std::move(result).error());
return;
Expand All @@ -715,6 +728,14 @@ QVariantMap PackageManager::installFromLayer(const QDBusUnixFileDescriptor &fd,
return;
}

utils::Transaction transaction;
transaction.addRollBack([this, newRef = *newRef, module]() noexcept {
auto res = repo->remove(newRef, module);
if (!res) {
LogW("failed to roll back importLayerDir {} {}", newRef.toString(), module);
}
});

auto ret = executePostInstallHooks(*newRef);
if (!ret) {
taskRef.reportError(std::move(ret).error());
Expand All @@ -725,12 +746,15 @@ QVariantMap PackageManager::installFromLayer(const QDBusUnixFileDescriptor &fd,
auto res = applyApp(*newRef);
if (!res) {
taskRef.reportError(std::move(res).error());
return;
}
transaction.commit();
return;
}

auto modules = this->repo->getModuleList(*localRef);
if (std::find(modules.cbegin(), modules.cend(), module) == modules.cend()) {
transaction.commit();
return;
}

Expand All @@ -741,6 +765,7 @@ QVariantMap PackageManager::installFromLayer(const QDBusUnixFileDescriptor &fd,
packageRef.toString(),
ret.error().message());
}
transaction.commit();
};

auto taskRet = tasks.addPackageTask(std::move(installer), ctx);
Expand Down Expand Up @@ -1195,11 +1220,21 @@ utils::error::Result<void> PackageManager::installRefModule(Task &task,
return LINGLONG_ERR(res);
}

utils::Transaction transaction;
transaction.addRollBack([this, &ref, module]() noexcept {
auto res = repo->remove(ref.reference, module);
if (!res) {
LogW("failed to roll back pull {} {}", ref.reference.toString(), module);
}
});

res = executePostInstallHooks(ref.reference);
if (!res) {
LogW(fmt::format("failed to execute postInstall hooks {}", ref.reference.toString()));
return LINGLONG_ERR("signature verification failed", res);
}
Comment thread
dengbo11 marked this conversation as resolved.

transaction.commit();

return LINGLONG_OK;
}

Expand Down Expand Up @@ -1242,9 +1277,16 @@ utils::error::Result<void> PackageManager::installRef(Task &task,
return LINGLONG_ERR(res);
}

transaction.addRollBack([this, &ref, module]() noexcept {
auto res = repo->remove(ref.reference, module);
if (!res) {
LogW("failed to roll back pull {} {}", ref.reference.toString(), module);
}
});

res = executePostInstallHooks(ref.reference);
if (!res) {
LogW(fmt::format("failed to execute postInstall hooks {}", ref.reference.toString()));
return LINGLONG_ERR("signature verification failed", res);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2026 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later

Expand All @@ -12,12 +12,16 @@
#include "linglong/utils/cmd.h"
#include "linglong/utils/error/error.h"

#include <nlohmann/json.hpp>

#include <QDir>
#include <QFile>
#include <QSharedPointer>

#include <filesystem>
#include <fstream>
#include <memory>
#include <sstream>
#include <string>

using namespace linglong;
Expand Down Expand Up @@ -182,4 +186,96 @@ TEST_F(LayerPackagerTest, InitWorkDir)
<< "workdir should be temporary directory";
}

TEST_F(LayerPackagerTest, ExtractSignData)
{
TempDir signTempDir("linglong-sign-test-");
ASSERT_TRUE(signTempDir.isValid());

auto signContentDir = signTempDir.path() / "sign";
std::filesystem::create_directories(signContentDir);
std::ofstream signFile(signContentDir / "sign_test_file");
signFile << "sign data content";
signFile.close();

auto signTarPath = signTempDir.path() / "sign.tar";
auto tarRet =
utils::Cmd("tar").exec({ "-cvf", signTarPath.c_str(), "-C", signContentDir.c_str(), "." });
ASSERT_TRUE(tarRet.has_value()) << "Failed to create sign.tar";

QFile originalLayer(layerFilePath.c_str());
ASSERT_TRUE(originalLayer.open(QIODevice::ReadOnly));

auto magic = originalLayer.read(40);
ASSERT_EQ(magic, magicNumber());

QDataStream metaLenStream(&originalLayer);
metaLenStream.setByteOrder(QDataStream::LittleEndian);
quint32 metaLen = 0;
metaLenStream >> metaLen;

auto metaData = originalLayer.read(metaLen);
auto metaJson = nlohmann::json::parse(metaData.toStdString());

auto erofsData = originalLayer.readAll();
auto erofsSize = static_cast<uint64_t>(erofsData.size());
originalLayer.close();

QFile signTarFile(signTarPath.c_str());
ASSERT_TRUE(signTarFile.open(QIODevice::ReadOnly));
auto signData = signTarFile.readAll();
auto signSize = static_cast<uint64_t>(signData.size());
signTarFile.close();

metaJson["erofs_size"] = erofsSize;
metaJson["sign_size"] = signSize;
auto newMetaData = QByteArray::fromStdString(metaJson.dump());
quint32 newMetaLen = static_cast<quint32>(newMetaData.size());

auto signedLayerPath = signTempDir.path() / "signed.layer";
QFile signedLayer(signedLayerPath.c_str());
ASSERT_TRUE(signedLayer.open(QIODevice::WriteOnly));

signedLayer.write(magicNumber());

QByteArray lenBytes;
QDataStream lenStream(&lenBytes, QIODevice::WriteOnly);
lenStream.setByteOrder(QDataStream::LittleEndian);
lenStream << newMetaLen;
signedLayer.write(lenBytes);
signedLayer.write(newMetaData);
signedLayer.write(erofsData);
signedLayer.write(signData);
signedLayer.close();

auto signedLayerFileRet = package::LayerFile::New(signedLayerPath.c_str());
ASSERT_TRUE(signedLayerFileRet.has_value())
<< "Failed to create layer file: " << signedLayerFileRet.error().message();
auto signedLayerFile = *signedLayerFileRet;

package::LayerPackager packager;
auto extractRet = packager.extractSignData(*signedLayerFile, signTempDir.path());
ASSERT_TRUE(extractRet.has_value()) << "Failed to extract sign data";

auto signDataDir = signTempDir.path() / "entries" / "share" / "deepin-elf-verify" / ".elfsign";
ASSERT_TRUE(std::filesystem::exists(signDataDir / "sign_test_file"))
<< "sign_test_file not found in " << signDataDir;

std::ifstream testFile(signDataDir / "sign_test_file");
std::stringstream buffer;
buffer << testFile.rdbuf();
ASSERT_EQ(buffer.str(), "sign data content");
}

TEST_F(LayerPackagerTest, ExtractSignDataNoSign)
{
auto layerFileRet = package::LayerFile::New(layerFilePath.c_str());
ASSERT_TRUE(layerFileRet.has_value())
<< "Failed to create layer file: " << layerFileRet.error().message();
auto layerFile = *layerFileRet;

package::LayerPackager packager;
auto extractRet = packager.extractSignData(*layerFile, packager.getWorkDir());
ASSERT_TRUE(extractRet.has_value()) << "extractSignData should succeed";
}

} // namespace linglong::package
Loading