Skip to content
Merged
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
53 changes: 47 additions & 6 deletions libmamba/src/core/link.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,7 @@ namespace mamba
bool run_script(
const TransactionParams& transaction_params,
const PrefixParams& prefix_params,
const fs::u8path& script_prefix,
const specs::PackageInfo& pkg_info,
const std::string& action = "post-link",
const std::string& env_prefix = "",
Expand All @@ -550,12 +551,12 @@ namespace mamba
fs::u8path path;
if (util::on_win)
{
path = prefix_params.target_prefix / get_bin_directory_short_path()
path = script_prefix / get_bin_directory_short_path()
/ util::concat(".", pkg_info.name, "-", action, ".bat");
}
else
{
path = prefix_params.target_prefix / get_bin_directory_short_path()
path = script_prefix / get_bin_directory_short_path()
/ util::concat(".", pkg_info.name, "-", action, ".sh");
}

Expand All @@ -566,14 +567,23 @@ namespace mamba
return true;
}

// TODO impl.
std::map<std::string, std::string> envmap; // = env::copy();

std::map<std::string, std::string> envmap;
Comment thread
Hind-M marked this conversation as resolved.
if (action == "pre-link")
{
throw std::runtime_error("mamba does not support pre-link scripts");
LOG_WARNING
<< "Special Note: Pre-link scripts are particularly high-risk as they can "
<< "modify the package cache, potentially affecting all environments on this system.";
envmap["SOURCE_DIR"] = script_prefix.string();
}

if (action == "post-unlink")
{
LOG_WARNING << "post-unlink scripts are deprecated and therefore won't be executed!";
Comment thread
Hind-M marked this conversation as resolved.
return true;
}
Comment thread
Hind-M marked this conversation as resolved.
Comment thread
Hind-M marked this conversation as resolved.

LOG_WARNING << "Executing " << action << " script for package '" << pkg_info.name << "'.";

// script_caller = None
std::vector<std::string> command_args;
std::unique_ptr<TemporaryFile> script_file;
Expand Down Expand Up @@ -799,6 +809,16 @@ namespace mamba
return true;
}

run_script(
m_context->transaction_params(),
m_context->prefix_params(),
m_context->prefix_params().target_prefix,
m_pkg_info,
"pre-unlink",
"",
false
);

std::ifstream json_file = open_ifstream(json);
if (!json_file.good())
{
Expand Down Expand Up @@ -848,6 +868,16 @@ namespace mamba

json_file.close();

run_script(
m_context->transaction_params(),
m_context->prefix_params(),
m_context->prefix_params().target_prefix,
m_pkg_info,
"post-unlink",
"",
true
);

fs::remove(json);

return true;
Expand Down Expand Up @@ -1167,6 +1197,16 @@ namespace mamba
nlohmann::json index_json, out_json;
LOG_TRACE << "Preparing linking from '" << m_source.string() << "'";

run_script(
m_context->transaction_params(),
m_context->prefix_params(),
m_source,
m_pkg_info,
"pre-link",
"",
false
);

LOG_TRACE << "Opening: " << m_source / "info" / "paths.json";
auto paths_data = read_paths(m_source);

Expand Down Expand Up @@ -1431,6 +1471,7 @@ namespace mamba
run_script(
m_context->transaction_params(),
m_context->prefix_params(),
m_context->prefix_params().target_prefix,
m_pkg_info,
"post-link",
"",
Expand Down
4 changes: 4 additions & 0 deletions libmamba/src/core/transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,10 @@ namespace mamba
return true;
}

LOG_WARNING
<< "Security Warning: This transaction includes executing package scripts (pre/post-link/unlink) if present. "
<< "These scripts can contain arbitrary code. Please ensure you trust the package sources.";

return Console::prompt("Confirm changes", 'y');
}

Expand Down
1 change: 1 addition & 0 deletions libmamba/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ set(
src/core/test_history.cpp
src/core/test_invoke.cpp
src/core/test_link_entry_points.cpp
src/core/test_link_scripts.cpp
src/core/test_lockfile.cpp
src/core/test_output.cpp
src/core/test_package_cache.cpp
Expand Down
172 changes: 172 additions & 0 deletions libmamba/tests/src/core/test_link_scripts.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright (c) 2026, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.

#include <fstream>

#include <catch2/catch_all.hpp>

#include "mamba/core/fsutil.hpp"
#include "mamba/core/util.hpp"
#include "mamba/core/util_os.hpp"
#include "mamba/specs/package_info.hpp"

#include "core/link.hpp"
#include "core/transaction_context.hpp"

#include "mambatests.hpp"

namespace mamba
{
namespace
{
TEST_CASE("run_scripts_in_transaction")
{
// Ensure Console is initialized to avoid singleton access errors
(void) mambatests::context();

const auto tmp_dir = TemporaryDirectory();
const fs::u8path prefix = tmp_dir.path() / "prefix";
const fs::u8path cache_dir = tmp_dir.path() / "cache";

specs::PackageInfo pkg("test_pkg");
pkg.version = "1.0";
pkg.build_string = "0";

const fs::u8path pkg_source = cache_dir / pkg.str();
const fs::u8path bin_dir_relative = get_bin_directory_short_path();

fs::create_directories(prefix / "conda-meta");
fs::create_directories(pkg_source / bin_dir_relative);
fs::create_directories(prefix / bin_dir_relative);

// Provide a dummy conda binary so wrap_call activation wrapper
// succeeds in the test environment
if (util::on_win)
{
fs::create_directories(prefix / "condabin");
auto conda_bat_path = prefix / "condabin"
/ (get_self_exe_path().stem().string() + ".bat");
auto out = open_ofstream(conda_bat_path);
out << "@exit /b 0\n";
}
else
{
fs::create_directories(prefix / "bin");
auto conda_path = prefix / "bin" / "conda";
auto out = open_ofstream(conda_path);
out << "#!/bin/sh\n";
Comment thread
Hind-M marked this conversation as resolved.
out << "if [ \"$1\" = \"shell.posix\" ] && [ \"$2\" = \"hook\" ]; then\n";
out << " echo 'conda() { :; }'\n"; // Define conda as a no-op function
out << "fi\n";
make_executable(conda_path);
}

const auto make_tx_context = [&]
{
TransactionParams tx_params{
.is_mamba_exe = false,
.json_output = false,
.verbosity = 0,
.shortcuts = false,
.envs_dirs = {},
.platform = "linux-64",
.prefix_params =
PrefixParams{
.target_prefix = prefix,
.root_prefix = prefix,
.conda_prefix = prefix,
.relocate_prefix = prefix,
},
.link_params = {},
.threads_params = {},
};

return TransactionContext(
tx_params,
{ "3.14.4", "3.14.4" },
"lib/python3.14/site-packages",
{}
);
};

std::string script_ext = util::on_win ? ".bat" : ".sh";
Comment thread
Hind-M marked this conversation as resolved.
std::string pre_link_name = ".test_pkg-pre-link" + script_ext;
std::string post_link_name = ".test_pkg-post-link" + script_ext;
std::string pre_unlink_name = ".test_pkg-pre-unlink" + script_ext;
std::string post_unlink_name = ".test_pkg-post-unlink" + script_ext;

auto create_script = [](const fs::u8path& p, const fs::u8path& marker_path)
{
std::ofstream out = open_ofstream(p);
if (util::on_win)
{
out << "@echo off\n";
out << "echo done > \"" << marker_path.string() << "\"\n";
}
else
{
out << "#!/bin/sh\n";
out << "echo done > \"" << marker_path.string() << "\"\n";
}
};

fs::u8path pre_link_marker = tmp_dir.path() / "pre_link_done.txt";
fs::u8path post_link_marker = tmp_dir.path() / "post_link_done.txt";
fs::u8path pre_unlink_marker = tmp_dir.path() / "pre_unlink_done.txt";
fs::u8path post_unlink_marker = tmp_dir.path() / "post_unlink_done.txt";

create_script(pkg_source / bin_dir_relative / pre_link_name, pre_link_marker);
create_script(prefix / bin_dir_relative / post_link_name, post_link_marker);
create_script(prefix / bin_dir_relative / pre_unlink_name, pre_unlink_marker);
create_script(prefix / bin_dir_relative / post_unlink_name, post_unlink_marker);

// Prepare metadata for unlinking
{
auto out = open_ofstream(prefix / "conda-meta" / (pkg.str() + ".json"));
out << R"({
"name": "test_pkg",
"version": "1.0",
"build_string": "0",
"paths_data": { "paths": [], "paths_version": 1 }
})";
}
// Prepare paths.json for linking
{
fs::create_directories(pkg_source / "info");
auto out = open_ofstream(pkg_source / "info" / "paths.json");
out << R"({ "paths": [], "paths_version": 1 })";
}
// Prepare repodata_record.json for linking
{
auto out = open_ofstream(pkg_source / "info" / "repodata_record.json");
out << R"({ "noarch": null })";
}

SECTION("linking executes pre-link and post-link")
{
auto tx_context = make_tx_context();
LinkPackage link_pkg(pkg, cache_dir, &tx_context);

REQUIRE(link_pkg.execute());

REQUIRE(fs::exists(pre_link_marker));
REQUIRE(fs::exists(post_link_marker));
}

SECTION("unlinking executes pre-unlink and post-unlink")
{
auto tx_context = make_tx_context();
UnlinkPackage unlink_pkg(pkg, cache_dir, &tx_context);

REQUIRE(unlink_pkg.execute());

REQUIRE(fs::exists(pre_unlink_marker));
// post-unlink script should not be executed as deprecated
REQUIRE(!fs::exists(post_unlink_marker));
}
}
}
} // namespace mamba
11 changes: 7 additions & 4 deletions micromamba/tests/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -2426,10 +2426,13 @@ def test_create_from_mirror_with_prefix(tmp_home, tmp_root_prefix, tmp_path):
for package in res["actions"]["LINK"]
)

# Verify no warnings in output
combined_output = result.stdout + result.stderr
assert "warning" not in combined_output.lower(), (
f"Unexpected warning in output:\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"
# Verify warning in output
assert (
helpers.find_message_in_json_logs(
res,
"Security Warning: This transaction includes executing package scripts (pre/post-link/unlink) if present.",
Comment thread
Hind-M marked this conversation as resolved.
)
is not None
)


Expand Down
Loading