Skip to content

feat: add layer signature extraction support#1706

Draft
dengbo11 wants to merge 1 commit into
OpenAtom-Linyaps:masterfrom
dengbo11:fix-layer-sign
Draft

feat: add layer signature extraction support#1706
dengbo11 wants to merge 1 commit into
OpenAtom-Linyaps:masterfrom
dengbo11:fix-layer-sign

Conversation

@dengbo11

Copy link
Copy Markdown
Collaborator
  1. Implement extractSignData in LayerPackager to extract signature data from layer files
  2. Modify installFromLayer to extract sign data and pass overlays to repo->importLayerDir
  3. Add transaction rollback for importLayerDir in installFromLayer and installRef
  4. Fix error handling in executePostInstallHooks: return error instead of logging
  5. Add unit tests for extractSignData with signed and unsigned layers

Log: Added signature extraction for layer installation, enabling signature verification and overlay application.

Influence:

  1. Test installation of layers with valid signatures
  2. Test installation of layers without signatures (should succeed)
  3. Test installation with corrupted or missing signature metadata
  4. Verify that signature overlay is correctly extracted and placed
  5. Test rollback on failure during signature extraction
  6. Verify error when post-install hooks fail due to signature mismatch
  7. Test cleanup of temporary signature files after installation

@deepin-ci-robot

Copy link
Copy Markdown
Collaborator

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: dengbo11

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces signature extraction functionality for layer packages, integrating it into the package installation process with overlay support, transaction rollbacks, and updated error handling. It also adds corresponding unit tests. The reviewer identified several critical issues, including a potential infinite loop on EOF during signature reading, unhandled JSON parsing exceptions that could crash the daemon, a security risk of arbitrary file writes when extracting the tarball as root, silently ignored extraction errors, and a missing rollback transaction in installRefModule upon verification failure.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +347 to +355
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)));
}

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)));
        }

Comment on lines +305 to +308
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);

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);

}
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() });

Comment thread libs/linglong/src/linglong/package_manager/package_manager.cpp
Comment thread libs/linglong/src/linglong/package_manager/package_manager.cpp
@dengbo11

Copy link
Copy Markdown
Collaborator Author

pre-commit.ci autofix

@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 15.62500% with 81 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...s/linglong/src/linglong/package/layer_packager.cpp 24.19% 17 Missing and 30 partials ⚠️
...g/src/linglong/package_manager/package_manager.cpp 0.00% 34 Missing ⚠️
Files with missing lines Coverage Δ
...g/src/linglong/package_manager/package_manager.cpp 0.75% <0.00%> (-0.03%) ⬇️
...s/linglong/src/linglong/package/layer_packager.cpp 24.87% <24.19%> (-0.30%) ⬇️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

1. Implement extractSignData in LayerPackager to extract signature data
from layer files
2. Modify installFromLayer to extract sign data and pass overlays to
repo->importLayerDir
3. Add transaction rollback for importLayerDir in installFromLayer and
installRef
4. Fix error handling in executePostInstallHooks: return error instead
of logging
5. Add unit tests for extractSignData with signed and unsigned layers

Log: Added signature extraction for layer installation, enabling
signature verification and overlay application.

Influence:
1. Test installation of layers with valid signatures
2. Test installation of layers without signatures (should succeed)
3. Test installation with corrupted or missing signature metadata
4. Verify that signature overlay is correctly extracted and placed
5. Test rollback on failure during signature extraction
6. Verify error when post-install hooks fail due to signature mismatch
7. Test cleanup of temporary signature files after installation
@deepin-ci-robot

Copy link
Copy Markdown
Collaborator

deepin pr auto review

★ 总体评分:45分

■ 【总体评价】

代码实现了从 layer 文件提取签名数据并增加事务回滚机制,但存在死循环缺陷和路径遍历安全漏洞
逻辑正确性因未处理文件读取 EOF 导致死循环扣15分,因存在2个中危安全漏洞扣30分,代码质量较差扣10分

■ 【详细分析】

  • 1.语法逻辑(存在严重错误)✕

layer_packager.cppextractSignData 函数中,使用 ::read(selfFd, buf.data(), bytesToRead) 循环读取签名数据时,仅处理了返回值为 -1 的情况,未处理返回值为 0(到达文件末尾)的情况。如果恶意构造的 layer 文件中 sign_size 大于实际剩余文件大小,read 会返回 0,此时内层循环写入 0 字节后 totalBytes 不变,外层循环条件 totalBytes > 0 永远为真,导致死循环。
潜在问题:触发死循环导致安装进程卡死,消耗 CPU 资源
建议:在 read 返回 0 时直接返回错误,提示文件异常或意外到达 EOF

  • 2.代码质量(较差)✕

layer_packager.cpppackage_manager.cpp 中,硬编码了相同的深层路径字符串 "entries" / "share" / "deepin-elf-verify" / ".elfsign"。此外,extractSignData 函数中手动使用 POSIX API 实现了文件拷贝逻辑,而项目中已引入 linglong/utils/file.h,手动实现增加了代码复杂度和维护成本。
潜在问题:路径硬编码导致后续修改容易遗漏引发不一致;手动实现文件 IO 增加出错概率
建议:将公共路径提取为静态常量;优先复用项目现有的文件操作工具类

  • 3.代码性能(无性能问题)✓

使用 4096 字节的栈上缓冲区进行分块读写,避免了大块内存分配,算法复杂度为 O(N),系统调用开销合理。

  • 4.代码安全(存在 2 个安全漏洞)✕

漏洞对比统计:新增漏洞 2 个,减少漏洞 0 个,持平 0 个
总体风险描述,由于未对 layer 文件内的 JSON 字段进行边界校验,且解压 tar 包时缺少安全选项,存在整数溢出越界读取和路径遍历覆盖文件的风险

  • 安全漏洞1(【中危】):整数溢出导致越界读取 在 extractSignData 函数中,signOffset 的计算依赖于用户可控的 JSON 字段 erofs_size。若攻击者构造恶意的 layer 文件,将 erofs_size 设置为极大值,会导致 number.size() + sizeof(quint32) + metaLen + erofsSize 在转为 int64_t 时发生整数溢出,使得 signOffset 变为负数或指向文件内错误位置,后续 read 调用将读取非预期的内存或文件内容 ——非常重要

  • 安全漏洞2(【中危】):Tar 路径遍历 在 extractSignData 函数中,调用 utils::Cmd("tar").exec 解压签名包时未使用安全限制选项。若攻击者构造恶意的 sign.tar,内部包含绝对路径(如 /etc/cron.d/backdoor)或使用 ../ 跳转(如 ../../../etc/passwd),tar 默认行为会将其解压到目标目录之外,可能覆盖系统关键文件或执行恶意代码 ——非常重要

  • 建议:对 erofs_sizesign_size 进行上限校验防止整数溢出;在 tar 命令中追加 --no-absolute-filenames--no-same-permissions 等安全参数限制解压行为

■ 【改进建议代码示例】

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);

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

    // 修复安全漏洞1:防止整数溢出
    auto baseOffset = number.size() + sizeof(quint32) + metaLen;
    if (erofsSize > static_cast<uint64_t>(std::numeric_limits<int64_t>::max()) - baseOffset) {
        return LINGLONG_ERR("erofs_size is too large, potential integer overflow");
    }
    auto signOffset = static_cast<int64_t>(baseOffset + 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)));
        }

        // 修复语法逻辑错误:处理 EOF 防止死循环
        if (readBytes == 0) {
            return LINGLONG_ERR("read sign data error: unexpected EOF");
        }

        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;

    // 修复安全漏洞2:限制 tar 解压行为,防止路径遍历
    auto ret = utils::Cmd("tar").exec({ "-xf", tarFile.string(), "-C", destination.string(), 
                                        "--no-absolute-filenames", "--no-same-permissions", "--no-same-owner" });
    if (!ret) {
        return LINGLONG_ERR(ret);
    }

    return LINGLONG_OK;
}

@deepin-ci-robot

Copy link
Copy Markdown
Collaborator

@dengbo11: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
github-pr-review-ci 024d1dd link true /test github-pr-review-ci

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here.

@dengbo11 dengbo11 requested review from myml and reddevillg June 29, 2026 14:25
@dengbo11 dengbo11 marked this pull request as draft June 30, 2026 01:44
@dengbo11 dengbo11 closed this Jul 1, 2026
@dengbo11 dengbo11 deleted the fix-layer-sign branch July 1, 2026 05:49
@dengbo11 dengbo11 restored the fix-layer-sign branch July 1, 2026 08:15
@dengbo11 dengbo11 reopened this Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants