From 870e0ea473b9ff12c4588ab67b9d4a49b4994274 Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Thu, 11 Jun 2026 11:06:33 +0200 Subject: [PATCH 1/9] Add execution of pre-link and pre-unlink scripts --- libmamba/src/core/link.cpp | 53 ++++++- libmamba/src/core/transaction.cpp | 3 + libmamba/tests/CMakeLists.txt | 1 + libmamba/tests/src/core/test_link_scripts.cpp | 148 ++++++++++++++++++ 4 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 libmamba/tests/src/core/test_link_scripts.cpp diff --git a/libmamba/src/core/link.cpp b/libmamba/src/core/link.cpp index 15667cd160..10ce64a939 100644 --- a/libmamba/src/core/link.cpp +++ b/libmamba/src/core/link.cpp @@ -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 = "", @@ -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"); } @@ -566,14 +567,23 @@ namespace mamba return true; } - // TODO impl. - std::map envmap; // = env::copy(); - 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."; } + if (action == "post-unlink") + { + LOG_WARNING << "post-unlink scripts are deprecated and therefore won't be executed!"; + return true; + } + + LOG_WARNING << "Executing " << action << " script for package '" << pkg_info.name << "'."; + + // TODO impl. + std::map envmap; // = env::copy(); + // script_caller = None std::vector command_args; std::unique_ptr script_file; @@ -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()) { @@ -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; @@ -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); @@ -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", "", diff --git a/libmamba/src/core/transaction.cpp b/libmamba/src/core/transaction.cpp index 3d665be546..514dc4083b 100644 --- a/libmamba/src/core/transaction.cpp +++ b/libmamba/src/core/transaction.cpp @@ -1172,6 +1172,9 @@ 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'); } diff --git a/libmamba/tests/CMakeLists.txt b/libmamba/tests/CMakeLists.txt index d0648265dc..db0e38d240 100644 --- a/libmamba/tests/CMakeLists.txt +++ b/libmamba/tests/CMakeLists.txt @@ -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 diff --git a/libmamba/tests/src/core/test_link_scripts.cpp b/libmamba/tests/src/core/test_link_scripts.cpp new file mode 100644 index 0000000000..6d18c53c6f --- /dev/null +++ b/libmamba/tests/src/core/test_link_scripts.cpp @@ -0,0 +1,148 @@ +// 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 + +#include + +#include "mamba/core/fsutil.hpp" +#include "mamba/core/util.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); + + 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"; + 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 From eabc89004420c4efcd9c7d763877e8289d640366 Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Thu, 11 Jun 2026 11:16:35 +0200 Subject: [PATCH 2/9] Fix linter --- libmamba/src/core/link.cpp | 5 +++-- libmamba/src/core/transaction.cpp | 5 +++-- libmamba/tests/src/core/test_link_scripts.cpp | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/libmamba/src/core/link.cpp b/libmamba/src/core/link.cpp index 10ce64a939..a5bcadf9db 100644 --- a/libmamba/src/core/link.cpp +++ b/libmamba/src/core/link.cpp @@ -569,8 +569,9 @@ namespace mamba if (action == "pre-link") { - 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."; + 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."; } if (action == "post-unlink") diff --git a/libmamba/src/core/transaction.cpp b/libmamba/src/core/transaction.cpp index 514dc4083b..3a5b402f53 100644 --- a/libmamba/src/core/transaction.cpp +++ b/libmamba/src/core/transaction.cpp @@ -1172,8 +1172,9 @@ 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."; + 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'); } diff --git a/libmamba/tests/src/core/test_link_scripts.cpp b/libmamba/tests/src/core/test_link_scripts.cpp index 6d18c53c6f..f883c4d22a 100644 --- a/libmamba/tests/src/core/test_link_scripts.cpp +++ b/libmamba/tests/src/core/test_link_scripts.cpp @@ -75,7 +75,8 @@ namespace mamba 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) { + auto create_script = [](const fs::u8path& p, const fs::u8path& marker_path) + { std::ofstream out = open_ofstream(p); if (util::on_win) { From a81e61880db87348366f21a775e0e7339f51195b Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Thu, 11 Jun 2026 15:25:39 +0200 Subject: [PATCH 3/9] Comment offending test to validate assumption of failure --- libmamba/tests/src/core/test_link_scripts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libmamba/tests/src/core/test_link_scripts.cpp b/libmamba/tests/src/core/test_link_scripts.cpp index f883c4d22a..d58e6ca8a3 100644 --- a/libmamba/tests/src/core/test_link_scripts.cpp +++ b/libmamba/tests/src/core/test_link_scripts.cpp @@ -130,7 +130,7 @@ namespace mamba REQUIRE(link_pkg.execute()); REQUIRE(fs::exists(pre_link_marker)); - REQUIRE(fs::exists(post_link_marker)); + // REQUIRE(fs::exists(post_link_marker)); } SECTION("unlinking executes pre-unlink and post-unlink") From 748cace4fb0163877d7ba50765ec66ea100be36c Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Thu, 11 Jun 2026 16:48:50 +0200 Subject: [PATCH 4/9] Try with is_mamba_exe as true --- libmamba/tests/src/core/test_link_scripts.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libmamba/tests/src/core/test_link_scripts.cpp b/libmamba/tests/src/core/test_link_scripts.cpp index d58e6ca8a3..fa1c0afbbc 100644 --- a/libmamba/tests/src/core/test_link_scripts.cpp +++ b/libmamba/tests/src/core/test_link_scripts.cpp @@ -44,7 +44,7 @@ namespace mamba const auto make_tx_context = [&] { TransactionParams tx_params{ - .is_mamba_exe = false, + .is_mamba_exe = true, // To enable activation for post-link script .json_output = false, .verbosity = 0, .shortcuts = false, @@ -130,7 +130,7 @@ namespace mamba REQUIRE(link_pkg.execute()); REQUIRE(fs::exists(pre_link_marker)); - // REQUIRE(fs::exists(post_link_marker)); + REQUIRE(fs::exists(post_link_marker)); } SECTION("unlinking executes pre-unlink and post-unlink") From 982a2694690fe676e6367a1a3c130e75e0f6f375 Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Fri, 12 Jun 2026 11:51:50 +0200 Subject: [PATCH 5/9] Create dummy conda_bat file for tests --- libmamba/tests/src/core/test_link_scripts.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/libmamba/tests/src/core/test_link_scripts.cpp b/libmamba/tests/src/core/test_link_scripts.cpp index fa1c0afbbc..f54f6cdc80 100644 --- a/libmamba/tests/src/core/test_link_scripts.cpp +++ b/libmamba/tests/src/core/test_link_scripts.cpp @@ -10,6 +10,7 @@ #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" @@ -40,11 +41,19 @@ namespace mamba fs::create_directories(prefix / "conda-meta"); fs::create_directories(pkg_source / bin_dir_relative); fs::create_directories(prefix / bin_dir_relative); + 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"; + } const auto make_tx_context = [&] { TransactionParams tx_params{ - .is_mamba_exe = true, // To enable activation for post-link script + .is_mamba_exe = false, .json_output = false, .verbosity = 0, .shortcuts = false, From 8131cd657c68d4bbe5bd16a7672746498d54b37d Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Fri, 12 Jun 2026 16:14:21 +0200 Subject: [PATCH 6/9] Add dummy conda binary for unix as well --- libmamba/tests/src/core/test_link_scripts.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libmamba/tests/src/core/test_link_scripts.cpp b/libmamba/tests/src/core/test_link_scripts.cpp index f54f6cdc80..f9cfb5ee7c 100644 --- a/libmamba/tests/src/core/test_link_scripts.cpp +++ b/libmamba/tests/src/core/test_link_scripts.cpp @@ -41,6 +41,9 @@ namespace mamba 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"); @@ -49,6 +52,17 @@ namespace mamba 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"; + 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 = [&] { From e2edf53e55ce93052f539f9f5ddf2ff442a4d0d1 Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Tue, 16 Jun 2026 16:55:48 +0200 Subject: [PATCH 7/9] Set SOURCE_DIR in envmap --- libmamba/src/core/link.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libmamba/src/core/link.cpp b/libmamba/src/core/link.cpp index a5bcadf9db..e706feea1f 100644 --- a/libmamba/src/core/link.cpp +++ b/libmamba/src/core/link.cpp @@ -567,11 +567,13 @@ namespace mamba return true; } + std::map envmap; if (action == "pre-link") { 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") @@ -582,9 +584,6 @@ namespace mamba LOG_WARNING << "Executing " << action << " script for package '" << pkg_info.name << "'."; - // TODO impl. - std::map envmap; // = env::copy(); - // script_caller = None std::vector command_args; std::unique_ptr script_file; From 59244a4323b69509b987e6376cb11ce4e70b6a03 Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Thu, 18 Jun 2026 16:24:10 +0200 Subject: [PATCH 8/9] Fix warning for json output --- libmamba/src/core/transaction.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libmamba/src/core/transaction.cpp b/libmamba/src/core/transaction.cpp index 3a5b402f53..3ec8d6e95a 100644 --- a/libmamba/src/core/transaction.cpp +++ b/libmamba/src/core/transaction.cpp @@ -1172,9 +1172,12 @@ 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."; + if (!ctx.output_params.json) + { + 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'); } From f434523e03b47ebaffd0beed098f3fa577c0bb36 Mon Sep 17 00:00:00 2001 From: Hind Montassif Date: Mon, 22 Jun 2026 13:53:13 +0200 Subject: [PATCH 9/9] Adapt test instead of condition for warning --- libmamba/src/core/transaction.cpp | 9 +++------ micromamba/tests/test_create.py | 11 +++++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/libmamba/src/core/transaction.cpp b/libmamba/src/core/transaction.cpp index 3ec8d6e95a..3a5b402f53 100644 --- a/libmamba/src/core/transaction.cpp +++ b/libmamba/src/core/transaction.cpp @@ -1172,12 +1172,9 @@ namespace mamba return true; } - if (!ctx.output_params.json) - { - 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."; - } + 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'); } diff --git a/micromamba/tests/test_create.py b/micromamba/tests/test_create.py index c398b9b228..5099870290 100644 --- a/micromamba/tests/test_create.py +++ b/micromamba/tests/test_create.py @@ -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.", + ) + is not None )