Version in base suite: 2.3.0-1+deb13u1 Base version: opam_2.3.0-1+deb13u1 Target version: opam_2.3.0-1+deb13u2 Base file: /srv/ftp-master.debian.org/ftp/pool/main/o/opam/opam_2.3.0-1+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/o/opam/opam_2.3.0-1+deb13u2.dsc changelog | 8 patches/0002-Add-tiny-Ounit-like-test-framework.patch | 199 ++++++++ patches/0003-Add-unit-tests-for-OpamFilename.starts_with-like-fun.patch | 241 +++++++++ patches/0004-Fix-OpamFilename.starts_with-and-dir_starts_with.patch | 54 ++ patches/0005-Add-a-test-ensuring-installing-files-through-a-.inst.patch | 173 +++++++ patches/0006-Fix-bypass-switch-prefix-check-on-install-using-syml.patch | 118 ++++ patches/0007-Fix-OpamSystem.real_path.patch | 94 +++ patches/0008-add-unit-test-for-OpamSystem.real_path.patch | 247 ++++++++++ patches/0009-Re-allow-.install-files-containing.patch | 40 + patches/0010-Stop-the-installation-process-via-.install-file-from.patch | 69 ++ patches/series | 9 11 files changed, 1251 insertions(+), 1 deletion(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp_1s2jq0o/opam_2.3.0-1+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp_1s2jq0o/opam_2.3.0-1+deb13u2.dsc: no acceptable signature found diff -Nru opam-2.3.0/debian/changelog opam-2.3.0/debian/changelog --- opam-2.3.0/debian/changelog 2026-04-16 13:55:31.000000000 +0000 +++ opam-2.3.0/debian/changelog 2026-07-10 14:51:19.000000000 +0000 @@ -1,7 +1,13 @@ +opam (2.3.0-1+deb13u2) trixie-security; urgency=medium + + * Backport upstream patches (Closes: CVE-2026-57825) + + -- Stéphane Glondu Fri, 10 Jul 2026 16:51:19 +0200 + opam (2.3.0-1+deb13u1) trixie-security; urgency=medium * Invalidate .install fields containing destination filepath trying to - escape their scope + escape their scope (Closes: CVE-2026-41082) -- Stéphane Glondu Thu, 16 Apr 2026 15:55:31 +0200 diff -Nru opam-2.3.0/debian/patches/0002-Add-tiny-Ounit-like-test-framework.patch opam-2.3.0/debian/patches/0002-Add-tiny-Ounit-like-test-framework.patch --- opam-2.3.0/debian/patches/0002-Add-tiny-Ounit-like-test-framework.patch 1970-01-01 00:00:00.000000000 +0000 +++ opam-2.3.0/debian/patches/0002-Add-tiny-Ounit-like-test-framework.patch 2026-07-10 14:51:13.000000000 +0000 @@ -0,0 +1,199 @@ +From: Nathan Rebours +Date: Wed, 3 Jun 2026 11:03:55 +0200 +Subject: Add tiny Ounit like test framework + +Signed-off-by: Nathan Rebours +--- + master_changes.md | 1 + + tests/opam-unit/dune | 2 + + tests/opam-unit/opamUnit.ml | 101 +++++++++++++++++++++ + .../opamSysPkg.mli => tests/opam-unit/opamUnit.mli | 36 ++++---- + 4 files changed, 124 insertions(+), 16 deletions(-) + create mode 100644 tests/opam-unit/dune + create mode 100644 tests/opam-unit/opamUnit.ml + copy src/format/opamSysPkg.mli => tests/opam-unit/opamUnit.mli (59%) + +diff --git a/master_changes.md b/master_changes.md +index 3787f18..da3aef9 100644 +--- a/master_changes.md ++++ b/master_changes.md +@@ -97,6 +97,7 @@ users) + ## Internal: Windows + + ## Test ++ * Add `opamUnit` as a basic unit test framework [#6953 @NathanReb] + + ## Benchmarks + +diff --git a/tests/opam-unit/dune b/tests/opam-unit/dune +new file mode 100644 +index 0000000..a1a66db +--- /dev/null ++++ b/tests/opam-unit/dune +@@ -0,0 +1,2 @@ ++(library ++ (name opamUnit)) +diff --git a/tests/opam-unit/opamUnit.ml b/tests/opam-unit/opamUnit.ml +new file mode 100644 +index 0000000..bc0c292 +--- /dev/null ++++ b/tests/opam-unit/opamUnit.ml +@@ -0,0 +1,101 @@ ++(**************************************************************************) ++(* *) ++(* Copyright 2026 OCamlPro *) ++(* *) ++(* All rights reserved. This file is distributed under the terms of the *) ++(* GNU Lesser General Public License version 2.1, with the special *) ++(* exception on linking described in the file LICENSE. *) ++(* *) ++(**************************************************************************) ++ ++type test = { ++ fun_name : string; ++ name : string; ++} ++ ++let pp_test fmt {fun_name; name} = ++ Format.fprintf fmt "%s.%s" fun_name name ++ ++type result = ++ | Failure of {expected : string; got : string} ++ | Skip of {reason : string} ++ | Success ++ ++type test_result = { ++ test : test; ++ result : result; ++} ++ ++type ctxt = { ++ mutable results : test_result list; ++ mutable failures : int; ++} ++ ++let failure ~ctxt ~fun_name ~test_name ~expected ~got () = ++ let test = {fun_name; name = test_name} in ++ ctxt.results <- {test; result = Failure {expected; got}}::ctxt.results; ++ ctxt.failures <- ctxt.failures + 1 ++ ++let success ~ctxt ~fun_name ~test_name () = ++ let test = {fun_name; name = test_name} in ++ ctxt.results <- {test; result = Success}::ctxt.results ++ ++let skip ~ctxt ~fun_name ~test_name ~reason () = ++ let test = {fun_name; name = test_name} in ++ ctxt.results <- {test; result = Skip {reason}}::ctxt.results ++ ++let print_failure ~test ~expected ~got = ++ Format.printf "[FAILED] %a: expected %s but got %s\n" ++ pp_test test expected got ++ ++let print_skip ~test ~reason = ++ Format.printf "[SKIPPED] %a: %s\n" pp_test test reason ++ ++let print_success ~test = ++ Format.printf "[OK] %a\n" pp_test test ++ ++let print_report ~show_failures ~show_skipped ~show_success ctxt = ++ List.iter ++ (function ++ | {test; result = Failure {expected; got}} -> ++ if show_failures then print_failure ~test ~expected ~got ++ | {test; result = Skip {reason}} -> ++ if show_skipped then print_skip ~test ~reason ++ | {test; result = Success} -> ++ if show_success then print_success ~test) ++ ctxt.results ++ ++module Args = struct ++ let show_skipped = ref false ++ let show_success= ref false ++ let hide_failures = ref false ++ ++ let specs = [ ++ ("--show-skipped", Arg.Set show_skipped , "Show skipped tests in report"); ++ ("--show-success", Arg.Set show_success , "Show succesful tests in report"); ++ ("--hide-failure", Arg.Set hide_failures, "Do not show failed tests in report"); ++ ] ++ ++ let usage_msg = ++ let exe_name = Filename.basename Sys.executable_name in ++ Printf.sprintf "%s [options]" exe_name ++ ++ let handle_pos_arg s = ++ raise (Arg.Bad (Printf.sprintf "No positional arguments accepted: %s" s)) ++ ++ let parse () = Arg.parse specs handle_pos_arg usage_msg ++end ++ ++let run_tests f = ++ Args.parse (); ++ let ctxt = {failures = 0; results = []} in ++ f ctxt; ++ ctxt.results <- List.rev ctxt.results; ++ print_report ++ ~show_failures:(not !Args.hide_failures) ++ ~show_skipped:!Args.show_skipped ++ ~show_success:!Args.show_success ++ ctxt; ++ match ctxt.failures with ++ | 0 -> exit 0 ++ | _ -> exit 1 +diff --git a/src/format/opamSysPkg.mli b/tests/opam-unit/opamUnit.mli +similarity index 59% +copy from src/format/opamSysPkg.mli +copy to tests/opam-unit/opamUnit.mli +index d0239df..e55b79f 100644 +--- a/src/format/opamSysPkg.mli ++++ b/tests/opam-unit/opamUnit.mli +@@ -1,6 +1,6 @@ + (**************************************************************************) + (* *) +-(* Copyright 2019 OCamlPro *) ++(* Copyright 2026 OCamlPro *) + (* *) + (* All rights reserved. This file is distributed under the terms of the *) + (* GNU Lesser General Public License version 2.1, with the special *) +@@ -8,22 +8,26 @@ + (* *) + (**************************************************************************) + +-(** System package *) +-type t +-include OpamStd.ABSTRACT with type t := t ++type ctxt + +-val raw_set: OpamStd.String.Set.t -> Set.t ++val failure : ++ ctxt: ctxt -> ++ fun_name: string -> ++ test_name: string -> ++ expected: string -> ++ got: string -> ++ unit -> ++ unit + +-(** System packages status *) +-type status = +- { +- s_available : Set.t; +- (** Package available but not installed *) ++val success : ++ ctxt: ctxt -> fun_name: string -> test_name: string -> unit -> unit + +- s_not_found : Set.t; +- (** Package unavailable on this system *) +- } ++val skip : ++ ctxt: ctxt -> ++ fun_name: string -> ++ test_name: string -> ++ reason: string -> ++ unit -> ++ unit + +-val status_empty: status +- +-val string_of_status: status -> string ++val run_tests : (ctxt -> unit) -> unit diff -Nru opam-2.3.0/debian/patches/0003-Add-unit-tests-for-OpamFilename.starts_with-like-fun.patch opam-2.3.0/debian/patches/0003-Add-unit-tests-for-OpamFilename.starts_with-like-fun.patch --- opam-2.3.0/debian/patches/0003-Add-unit-tests-for-OpamFilename.starts_with-like-fun.patch 1970-01-01 00:00:00.000000000 +0000 +++ opam-2.3.0/debian/patches/0003-Add-unit-tests-for-OpamFilename.starts_with-like-fun.patch 2026-07-10 14:51:13.000000000 +0000 @@ -0,0 +1,241 @@ +From: Nathan Rebours +Date: Wed, 3 Jun 2026 11:04:20 +0200 +Subject: Add unit tests for OpamFilename.starts_with-like functions + +Signed-off-by: Nathan Rebours +--- + master_changes.md | 1 + + tests/lib/core/dune | 4 + + .../lib/core/testOpamCore.ml | 7 +- + tests/lib/core/testOpamFilename.ml | 151 +++++++++++++++++++++ + .../lib/core/testOpamFilename.mli | 4 +- + 5 files changed, 163 insertions(+), 4 deletions(-) + create mode 100644 tests/lib/core/dune + copy src/solver/opamBuiltinMccs.mli => tests/lib/core/testOpamCore.ml (80%) + create mode 100644 tests/lib/core/testOpamFilename.ml + copy src/solver/opamBuiltinMccs.mli => tests/lib/core/testOpamFilename.mli (84%) + +diff --git a/master_changes.md b/master_changes.md +index da3aef9..1233e6f 100644 +--- a/master_changes.md ++++ b/master_changes.md +@@ -98,6 +98,7 @@ users) + + ## Test + * Add `opamUnit` as a basic unit test framework [#6953 @NathanReb] ++ * Add unit tests for `OpamFilename.starts_with` and `dir_starts_with` in `tests/lib/core` [#6953 @NathanReb] + + ## Benchmarks + +diff --git a/tests/lib/core/dune b/tests/lib/core/dune +new file mode 100644 +index 0000000..949cbe1 +--- /dev/null ++++ b/tests/lib/core/dune +@@ -0,0 +1,4 @@ ++(test ++ (name testOpamCore) ++ (package opam-core) ++ (libraries opam-core opamUnit)) +diff --git a/src/solver/opamBuiltinMccs.mli b/tests/lib/core/testOpamCore.ml +similarity index 80% +copy from src/solver/opamBuiltinMccs.mli +copy to tests/lib/core/testOpamCore.ml +index 32c791b..11770b7 100644 +--- a/src/solver/opamBuiltinMccs.mli ++++ b/tests/lib/core/testOpamCore.ml +@@ -1,6 +1,6 @@ + (**************************************************************************) + (* *) +-(* Copyright 2017-2018 OCamlPro *) ++(* Copyright 2026 OCamlPro *) + (* *) + (* All rights reserved. This file is distributed under the terms of the *) + (* GNU Lesser General Public License version 2.1, with the special *) +@@ -8,4 +8,7 @@ + (* *) + (**************************************************************************) + +-val all_backends: (module OpamCudfSolverSig.S) list ++let () = ++ OpamUnit.run_tests ++ (fun ctxt -> ++ TestOpamFilename.test ~ctxt ()) +diff --git a/tests/lib/core/testOpamFilename.ml b/tests/lib/core/testOpamFilename.ml +new file mode 100644 +index 0000000..57221f0 +--- /dev/null ++++ b/tests/lib/core/testOpamFilename.ml +@@ -0,0 +1,151 @@ ++(**************************************************************************) ++(* *) ++(* Copyright 2026 OCamlPro *) ++(* *) ++(* All rights reserved. This file is distributed under the terms of the *) ++(* GNU Lesser General Public License version 2.1, with the special *) ++(* exception on linking described in the file LICENSE. *) ++(* *) ++(**************************************************************************) ++ ++let test_starts_with ~ctxt () = ++ let fun_name = "OpamFilename.starts_with" in ++ let test ~on ~prefix_dir ~file ~expected = ++ let test_name = Printf.sprintf "(%s, %s)" prefix_dir file in ++ if List.exists (String.equal Sys.os_type) on then ++ (let prefix_dir' = OpamFilename.raw_dir prefix_dir in ++ let file' = OpamFilename.raw file in ++ let got = OpamFilename.starts_with prefix_dir' file' in ++ if Bool.equal expected got then ++ OpamUnit.success ~ctxt ~fun_name ~test_name () ++ else ++ let expected = Bool.to_string expected in ++ let got = Bool.to_string got in ++ OpamUnit.failure ~ctxt ~fun_name ~test_name ~expected ~got ()) ++ else ++ let reason = Printf.sprintf "Test does not run on %s" Sys.os_type in ++ OpamUnit.skip ~ctxt ~fun_name ~test_name ~reason () ++ in ++ List.iter ++ (fun (on, prefix_dir, file, expected) -> ++ test ~on ~prefix_dir ~file ~expected) ++ [ ++ (* absolute path without parent dirname *) ++ ["Unix"; "Cygwin"], "/no/match", "/foo/bar/file", false; ++ ["Unix"; "Cygwin"], "/foo/bar", "/foo/bar/file", true; ++ ["Unix"; "Cygwin"], "/foo/bar", "/foo/bar/baz/file", true; ++ ["Unix"; "Cygwin"], "/foo/bar", "/foo/bar-baz/file", false; ++ ["Unix"; "Cygwin"], "/foo/bar/", "/foo/bar-baz/file", false; ++ ["Unix"; "Cygwin"], "/foo/bar/file", "/foo/bar/file", false; ++ ["Cygwin"], "/cygdrive/c/foo/bar", "/cygdrive/c/foo/bar/file", true; ++ ["Cygwin"], "/cygdrive/c/foo/bar", "/cygdrive/d/foo/bar/file", false; ++ ["Win32"], {|C:\no\match|}, {|C:\foo\bar\file|}, false; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\bar\file|}, true; ++ ["Win32"], {|C:\foo\bar|}, {|D:\foo\bar\file|}, false; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\bar\baz\file|}, true; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\bar-baz\file|}, false; ++ ["Win32"], {|C:\foo\bar\|}, {|C:\foo\bar-baz\file|}, false; ++ ["Win32"], {|C:\foo\bar\file|}, {|C:\foo\bar\file|}, false; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\bar/file|}, true; ++ (* from current directory *) ++ (* Note that those tests exhibit how starts_with behaves with [.] ++ and [..] when constructed with OpamFilename.raw rather than specify ++ what the ideal behaviour is. *) ++ ["Unix"; "Cygwin"], "/no/match", "./foo/bar/file", false; ++ ["Unix"; "Cygwin"], "/foo/bar", "./foo/bar/file", false; ++ ["Unix"; "Cygwin"], "./foo/bar", "./foo/bar/file", true; ++ ["Win32"], {|C:\no\match|}, {|.\foo\bar\file|}, false; ++ ["Win32"], {|C:\foo\bar|}, {|.\foo\bar\file|}, false; ++ ["Win32"], {|.\foo\bar|}, {|.\foo\bar\file|}, true; ++ (* current directory in the middle *) ++ ["Unix"; "Cygwin"], "/foo/bar", "/foo/./bar/file", false; ++ ["Unix"; "Cygwin"], "/foo/bar", "/foo/bar/./file", true; ++ ["Unix"; "Cygwin"], "/foo/./bar", "/foo/./bar/file", true; ++ ["Unix"; "Cygwin"], "/foo/./bar", "/foo/./bar/./file", true; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\.\bar\file|}, false; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\bar\.\file|}, true; ++ ["Win32"], {|C:\foo\.\bar|}, {|C:\foo\.\bar\file|}, true; ++ ["Win32"], {|C:\foo\.\bar|}, {|C:\foo\.\bar\.\file|}, true; ++ (* absolute path with parent dirname *) ++ ["Unix"; "Cygwin"], "/foo/bar", "/foo/../bar/file", false; ++ ["Unix"; "Cygwin"], "/foo/bar", "/foo/bar/../file", true; ++ ["Unix"; "Cygwin"], "/foo/../bar", "/foo/../bar/file", true; ++ ["Unix"; "Cygwin"], "/foo/../bar", "/foo/../bar/../file", true; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\..\bar\file|}, false; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\bar\..\file|}, true; ++ ["Win32"], {|C:\foo\..\bar|}, {|C:\foo\..\bar\file|}, true; ++ ["Win32"], {|C:\foo\..\bar|}, {|C:\foo\..\bar\..\file|}, true; ++ ] ++ ++let test_dir_starts_with ~ctxt () = ++ let fun_name = "OpamFilename.dir_starts_with" in ++ let test ~on ~prefix_dir ~dir ~expected = ++ let test_name = Printf.sprintf "(%s, %s)" prefix_dir dir in ++ if List.exists (String.equal Sys.os_type) on then ++ (let prefix_dir' = OpamFilename.raw_dir prefix_dir in ++ let dir' = OpamFilename.raw_dir dir in ++ let got = OpamFilename.dir_starts_with prefix_dir' dir' in ++ if Bool.equal expected got then ++ OpamUnit.success ~ctxt ~fun_name ~test_name () ++ else ++ let expected = Bool.to_string expected in ++ let got = Bool.to_string got in ++ OpamUnit.failure ~ctxt ~fun_name ~test_name ~expected ~got ()) ++ else ++ let reason = Printf.sprintf "Test does not run on %s" Sys.os_type in ++ OpamUnit.skip ~ctxt ~fun_name ~test_name ~reason () ++ in ++ List.iter ++ (fun (on, prefix_dir, dir, expected) -> ++ test ~on ~prefix_dir ~dir ~expected) ++ [ ++ (* absolute path without parent dirname *) ++ ["Unix"; "Cygwin"], "/no/match", "/foo/bar/dir", false; ++ ["Unix"; "Cygwin"], "/foo/bar", "/foo/bar/dir", true; ++ ["Unix"; "Cygwin"], "/foo/bar", "/foo/bar/baz/dir", true; ++ ["Unix"; "Cygwin"], "/foo/bar", "/foo/bar-baz/dir", false; ++ ["Unix"; "Cygwin"], "/foo/bar/", "/foo/bar-baz/dir", false; ++ ["Unix"; "Cygwin"], "/foo/bar/dir", "/foo/bar/dir", true; ++ ["Cygwin"], "/cygdrive/c/foo/bar", "/cygdrive/c/foo/bar/dir", true; ++ ["Cygwin"], "/cygdrive/c/foo/bar", "/cygdrive/d/foo/bar/dir", false; ++ ["Win32"], {|C:\no\match|}, {|C:\foo\bar\dir|}, false; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\bar\dir|}, true; ++ ["Win32"], {|C:\foo\bar|}, {|D:\foo\bar\dir|}, false; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\bar\baz\dir|}, true; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\bar-baz\dir|}, false; ++ ["Win32"], {|C:\foo\bar\|}, {|C:\foo\bar-baz\dir|}, false; ++ ["Win32"], {|C:\foo\bar\dir|}, {|C:\foo\bar\dir|}, true; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\bar/dir|}, true; ++ (* from current directory *) ++ (* Note that those tests exhibit how dir_starts_with behaves with [.] ++ and [..] when constructed with OpamFilename.raw rather than specify ++ what the ideal behaviour is. *) ++ ["Unix"; "Cygwin"], "/no/match", "./foo/bar/dir", false; ++ ["Unix"; "Cygwin"], "/foo/bar", "./foo/bar/dir", false; ++ ["Unix"; "Cygwin"], "./foo/bar", "./foo/bar/dir", true; ++ ["Win32"], {|C:\no\match|}, {|.\foo\bar\dir|}, false; ++ ["Win32"], {|C:\foo\bar|}, {|.\foo\bar\dir|}, false; ++ ["Win32"], {|.\foo\bar|}, {|.\foo\bar\dir|}, true; ++ (* current directory in the middle *) ++ ["Unix"; "Cygwin"], "/foo/bar", "/foo/./bar/dir", false; ++ ["Unix"; "Cygwin"], "/foo/bar", "/foo/bar/./dir", true; ++ ["Unix"; "Cygwin"], "/foo/./bar", "/foo/./bar/dir", true; ++ ["Unix"; "Cygwin"], "/foo/./bar", "/foo/./bar/./dir", true; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\.\bar\dir|}, false; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\bar\.\dir|}, true; ++ ["Win32"], {|C:\foo\.\bar|}, {|C:\foo\.\bar\dir|}, true; ++ ["Win32"], {|C:\foo\.\bar|}, {|C:\foo\.\bar\.\dir|}, true; ++ (* absolute path with parent dirname *) ++ ["Unix"; "Cygwin"], "/foo/bar", "/foo/../bar/dir", false; ++ ["Unix"; "Cygwin"], "/foo/bar", "/foo/bar/../dir", true; ++ ["Unix"; "Cygwin"], "/foo/../bar", "/foo/../bar/dir", true; ++ ["Unix"; "Cygwin"], "/foo/../bar", "/foo/../bar/../dir", true; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\..\bar\dir|}, false; ++ ["Win32"], {|C:\foo\bar|}, {|C:\foo\bar\..\dir|}, true; ++ ["Win32"], {|C:\foo\..\bar|}, {|C:\foo\..\bar\dir|}, true; ++ ["Win32"], {|C:\foo\..\bar|}, {|C:\foo\..\bar\..\dir|}, true; ++ ] ++ ++let test ~ctxt () = ++ test_starts_with ~ctxt (); ++ test_dir_starts_with ~ctxt () +diff --git a/src/solver/opamBuiltinMccs.mli b/tests/lib/core/testOpamFilename.mli +similarity index 84% +copy from src/solver/opamBuiltinMccs.mli +copy to tests/lib/core/testOpamFilename.mli +index 32c791b..6385587 100644 +--- a/src/solver/opamBuiltinMccs.mli ++++ b/tests/lib/core/testOpamFilename.mli +@@ -1,6 +1,6 @@ + (**************************************************************************) + (* *) +-(* Copyright 2017-2018 OCamlPro *) ++(* Copyright 2026 OCamlPro *) + (* *) + (* All rights reserved. This file is distributed under the terms of the *) + (* GNU Lesser General Public License version 2.1, with the special *) +@@ -8,4 +8,4 @@ + (* *) + (**************************************************************************) + +-val all_backends: (module OpamCudfSolverSig.S) list ++val test : ctxt: OpamUnit.ctxt -> unit -> unit diff -Nru opam-2.3.0/debian/patches/0004-Fix-OpamFilename.starts_with-and-dir_starts_with.patch opam-2.3.0/debian/patches/0004-Fix-OpamFilename.starts_with-and-dir_starts_with.patch --- opam-2.3.0/debian/patches/0004-Fix-OpamFilename.starts_with-and-dir_starts_with.patch 1970-01-01 00:00:00.000000000 +0000 +++ opam-2.3.0/debian/patches/0004-Fix-OpamFilename.starts_with-and-dir_starts_with.patch 2026-07-10 14:51:13.000000000 +0000 @@ -0,0 +1,54 @@ +From: Nathan Rebours +Date: Thu, 4 Jun 2026 11:02:55 +0200 +Subject: Fix OpamFilename.starts_with and dir_starts_with + +Signed-off-by: Nathan Rebours +--- + master_changes.md | 3 +++ + src/core/opamFilename.ml | 22 +++++++++++++++++----- + 2 files changed, 20 insertions(+), 5 deletions(-) + +diff --git a/master_changes.md b/master_changes.md +index 1233e6f..f99336d 100644 +--- a/master_changes.md ++++ b/master_changes.md +@@ -125,3 +125,6 @@ users) + ## opam-format + + ## opam-core ++ * `OpamFilename.{,dir_}starts_with`: Fix a bug where `foo/bar` would be considered a prefix of `foo/bar-baz` [#6953 @NathanReb - fix #6948] ++ * `OpamFilename.{,dir_}starts_with`: `/` and `\` are now equivalent on Windows [#6953 @NathanReb] ++ * `OpamFilename.starts_with`: `starts_with "a/b" "a/b"` no longer returns `true` [#6953 @NathanReb] +diff --git a/src/core/opamFilename.ml b/src/core/opamFilename.ml +index e94fbba..97e9d28 100644 +--- a/src/core/opamFilename.ml ++++ b/src/core/opamFilename.ml +@@ -331,11 +331,23 @@ let is_exec file = + with Unix.Unix_error _ -> + OpamSystem.internal_error "%s does not exist." (to_string file) + +-let starts_with dirname filename = +- OpamStd.String.starts_with ~prefix:(Dir.to_string dirname) (to_string filename) +- +-let dir_starts_with pfx dir = +- OpamStd.String.starts_with ~prefix:(Dir.to_string pfx) (Dir.to_string dir) ++let cmpable_dir_string dir = ++ match OpamSystem.forward_to_back (Dir.to_string dir) with ++ | "" -> "" ++ | d -> Filename.concat d "" ++ ++let cmpable_file_string file = ++ OpamSystem.forward_to_back (to_string file) ++ ++let starts_with prefix filename = ++ let prefix = cmpable_dir_string prefix in ++ let dir = cmpable_file_string filename in ++ OpamStd.String.starts_with ~prefix dir ++ ++let dir_starts_with prefix dir = ++ let prefix = cmpable_dir_string prefix in ++ let dir = cmpable_dir_string dir in ++ OpamStd.String.starts_with ~prefix dir + + let remove_prefix prefix filename = + let prefix = diff -Nru opam-2.3.0/debian/patches/0005-Add-a-test-ensuring-installing-files-through-a-.inst.patch opam-2.3.0/debian/patches/0005-Add-a-test-ensuring-installing-files-through-a-.inst.patch --- opam-2.3.0/debian/patches/0005-Add-a-test-ensuring-installing-files-through-a-.inst.patch 1970-01-01 00:00:00.000000000 +0000 +++ opam-2.3.0/debian/patches/0005-Add-a-test-ensuring-installing-files-through-a-.inst.patch 2026-07-10 14:51:13.000000000 +0000 @@ -0,0 +1,173 @@ +From: Nathan Rebours +Date: Mon, 6 Jul 2026 20:37:45 +0100 +Subject: Add a test ensuring installing files through a .install file can't + escape the opam switch (CVE-2026-57825) + +--- + master_changes.md | 1 + + tests/reftests/cve-2026-57825.test | 111 +++++++++++++++++++++++++++++++++++++ + tests/reftests/dune.inc | 21 +++++++ + 3 files changed, 133 insertions(+) + create mode 100644 tests/reftests/cve-2026-57825.test + +diff --git a/master_changes.md b/master_changes.md +index f99336d..42a8984 100644 +--- a/master_changes.md ++++ b/master_changes.md +@@ -104,6 +104,7 @@ users) + + ## Reftests + ### Tests ++ * Add a test ensuring installing files through a .install file can't escape the opam switch (CVE-2026-57825) [#7005 @NathanReb] + + ### Engine + +diff --git a/tests/reftests/cve-2026-57825.test b/tests/reftests/cve-2026-57825.test +new file mode 100644 +index 0000000..7ab0023 +--- /dev/null ++++ b/tests/reftests/cve-2026-57825.test +@@ -0,0 +1,111 @@ ++N0REP0 ++### : CVE-2026-57825 ++### : It used to be possible to create a symlink to a directory outside the opam ++### : switch and have the `.install` file make opam install a file within that ++### : directory, effectively making it install a file outside the switch. ++### : The proper behaviour is to detect this and gracefully fail, reporting the ++### : unauthorized attempt to install a file outside the authorized switch. ++### : Note that this can potentially also be used to install files in the switch ++### : meta dir which should also be prevented. ++### : ============= 1. Install outside switch through symlink dir. ============ ++### : Our package install command creates a symlink inside the switch, pointing ++### : to a directory outside the switch. ++### ++opam-version: "2.0" ++install: ++ ["sh" "-c" "ln -s \"$BASEDIR\" '%{lib}%/path'"] ++### ++I'll sneak outside the switch dir! ++### : The .install file would have us install a file in that directory, ++### : resulting in the described exploit. ++### ++lib_root: [ ++ "a-file" {"path/escapist"} ++] ++### opam switch create escapability-w-symlinks --empty ++### : The following install should fail. ++### opam install outside-symlink -vv | sed-cmd sh ++The following actions will be performed: ++=== install 1 package ++ - install outside-symlink 1 ++ ++<><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><><><> ++Processing 3/3: [outside-symlink: sh] +++ sh "-c" "ln -s \"$BASEDIR\" '${BASEDIR}/OPAM/escapability-w-symlinks/lib/path'" (CWD=${BASEDIR}/OPAM/escapability-w-symlinks/.opam-switch/build/outside-symlink.1) ++-> installed outside-symlink.1 ++Done. ++### : outside-symlink should not appear here ++### opam list -si outside-symlink ++outside-symlink ++### : No [escapist] file should appear in the test directory ++### ls $BASEDIR | grep escapist ++escapist ++### : ============= 2. Install in switch meta through symlink dir. ============ ++### : Our package install command creates a symlink inside the switch, pointing ++### : to the switch meta dir [.opam-switch]. ++### ++opam-version: "2.0" ++install: ++ ["sh" "-c" "ln -s '%{prefix}%/.opam-switch' '%{lib}%/path2'"] ++### ++I'll sneak into opam's meta dir! ++### : The .install file would have us install a file in that directory, ++### : resulting in the described exploit. ++### ++lib_root: [ ++ "b-file" {"path2/escapist"} ++] ++### : Trying to install the package should fail ++### opam install meta-symlink -vv | sed-cmd sh ++The following actions will be performed: ++=== install 1 package ++ - install meta-symlink 1 ++ ++<><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><><><> ++Processing 3/3: [meta-symlink: sh] +++ sh "-c" "ln -s '${BASEDIR}/OPAM/escapability-w-symlinks/.opam-switch' '${BASEDIR}/OPAM/escapability-w-symlinks/lib/path2'" (CWD=${BASEDIR}/OPAM/escapability-w-symlinks/.opam-switch/build/meta-symlink.1) ++-> installed meta-symlink.1 ++Done. ++### : meta-symlink should not appear here ++### opam list -si meta-symlink ++meta-symlink ++### : And the [to] file should not appear in the switch meta dir ++### ls $OPAMROOT/escapability-w-symlinks/.opam-switch | grep escapist ++escapist ++### : ============= 3. Install inside switch through symlink dir. ============ ++### : We also test that legitimate links are still permitted, i.e. ++### : links across opam dirs. ++### : Here the install command creates a link to `sbin` in bin/path ++### ++opam-version: "2.0" ++install: [ ++ ["sh" "-c" "ln -s '%{sbin}%' '%{bin}%/path'"] ++] ++### ++I'll sneak into opam's sbin! ++### : The .install file installs a file as bin/path/to which makes us ++### : effectively install it in sbin ++### ++bin: [ ++ "a-file" {"path/to"} ++] ++### : The install should go through ++### opam install good-symlink -vv | sed-cmd sh ++The following actions will be performed: ++=== install 1 package ++ - install good-symlink 1 ++ ++<><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><><><> ++Processing 3/3: [good-symlink: sh] +++ sh "-c" "ln -s '${BASEDIR}/OPAM/escapability-w-symlinks/sbin' '${BASEDIR}/OPAM/escapability-w-symlinks/bin/path'" (CWD=${BASEDIR}/OPAM/escapability-w-symlinks/.opam-switch/build/good-symlink.1) ++-> installed good-symlink.1 ++Done. ++### : good-symlink should be listed as installed ++### opam list -si good-symlink ++good-symlink ++### : the sbin symlink should have been successfully created ++### realpath -mLP OPAM/escapability-w-symlinks/bin/path ++${BASEDIR}/OPAM/escapability-w-symlinks/sbin ++### : The file [to] should appear in sbin ++### ls $OPAMROOT/escapability-w-symlinks/sbin ++to +diff --git a/tests/reftests/dune.inc b/tests/reftests/dune.inc +index 643bbb9..d77f328 100644 +--- a/tests/reftests/dune.inc ++++ b/tests/reftests/dune.inc +@@ -440,6 +440,27 @@ + %{targets} + (run ./run.exe %{exe:../../src/client/opamMain.exe.exe} %{dep:deprecated.test} %{read-lines:testing-env})))) + ++(rule ++ (alias reftest-cve-2026-57825) ++ (enabled_if (and (or (<> %{env:TESTALL=1} 0) (= %{env:TESTN0REP0=0} 1)))) ++ (action ++ (diff cve-2026-57825.test cve-2026-57825.out))) ++ ++(alias ++ (name reftest) ++ (enabled_if (and (or (<> %{env:TESTALL=1} 0) (= %{env:TESTN0REP0=0} 1)))) ++ (deps (alias reftest-cve-2026-57825))) ++ ++(rule ++ (targets cve-2026-57825.out) ++ (deps root-N0REP0) ++ (enabled_if (and (or (<> %{env:TESTALL=1} 0) (= %{env:TESTN0REP0=0} 1)))) ++ (package opam) ++ (action ++ (with-stdout-to ++ %{targets} ++ (run ./run.exe %{exe:../../src/client/opamMain.exe.exe} %{dep:cve-2026-57825.test} %{read-lines:testing-env})))) ++ + (rule + (alias reftest-deps-only) + (enabled_if (and (or (<> %{env:TESTALL=1} 0) (= %{env:TESTN0REP0=0} 1)))) diff -Nru opam-2.3.0/debian/patches/0006-Fix-bypass-switch-prefix-check-on-install-using-syml.patch opam-2.3.0/debian/patches/0006-Fix-bypass-switch-prefix-check-on-install-using-syml.patch --- opam-2.3.0/debian/patches/0006-Fix-bypass-switch-prefix-check-on-install-using-syml.patch 1970-01-01 00:00:00.000000000 +0000 +++ opam-2.3.0/debian/patches/0006-Fix-bypass-switch-prefix-check-on-install-using-syml.patch 2026-07-10 14:51:13.000000000 +0000 @@ -0,0 +1,118 @@ +From: Nathan Rebours +Date: Mon, 6 Jul 2026 20:39:15 +0100 +Subject: Fix bypass switch prefix check on install using symlinks + (CVE-2026-57825) + +--- + master_changes.md | 1 + + src/client/opamAction.ml | 19 ++++++++++++++++++- + tests/reftests/cve-2026-57825.test | 32 ++++++++++++++++++++++++-------- + 3 files changed, 43 insertions(+), 9 deletions(-) + +diff --git a/master_changes.md b/master_changes.md +index 42a8984..a320bcf 100644 +--- a/master_changes.md ++++ b/master_changes.md +@@ -113,6 +113,7 @@ users) + ## Doc + + ## Security fixes ++ * Fix a bug that allowed a package to install files anywhere on the system using a symlink to an external directory without warning the user and asking for their permission: CVE-2026-57825. [#7005 @NathanReb] + + # API updates + ## opam-client +diff --git a/src/client/opamAction.ml b/src/client/opamAction.ml +index 9fc844e..9effbba 100644 +--- a/src/client/opamAction.ml ++++ b/src/client/opamAction.ml +@@ -25,6 +25,7 @@ let preprocess_dot_install_t st nv build_dir = + if not (OpamFilename.exists_dir build_dir) then [], None else + let root = st.switch_global.root in + let switch_prefix = OpamPath.Switch.root root st.switch in ++ let switch_meta = OpamPath.Switch.meta root st.switch in + let file_wo_prefix f = OpamFilename.remove_prefix switch_prefix f in + + let name = nv.name in +@@ -112,7 +113,23 @@ let preprocess_dot_install_t st nv build_dir = + let inst warning = + if append then warning (OpamFilename.to_string src_file) `Add_exe; + let check, warn = check ~src:build_dir ~dst:dst_dir base in +- if check then ++ let real_dst = ++ OpamFilename.of_string ++ (OpamSystem.real_path (OpamFilename.to_string dst_file)) ++ in ++ let escapes = ++ not (OpamFilename.starts_with switch_prefix real_dst) ++ || (OpamFilename.starts_with switch_meta real_dst) ++ in ++ if escapes then ++ (OpamConsole.error ++ "package %s attempted to install a file outside allowed \ ++ destination directories: %s -> %s." ++ (OpamPackage.Name.to_string name) ++ (OpamFilename.to_string dst_file) ++ (OpamFilename.to_string real_dst); ++ failwith ".install file escaping allowed directories") ++ else if check then + OpamFilename.install ~warning ~exec ~src:src_file ~dst:dst_file (); + warn + in +diff --git a/tests/reftests/cve-2026-57825.test b/tests/reftests/cve-2026-57825.test +index 7ab0023..bcc7c60 100644 +--- a/tests/reftests/cve-2026-57825.test ++++ b/tests/reftests/cve-2026-57825.test +@@ -32,14 +32,22 @@ The following actions will be performed: + <><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><><><> + Processing 3/3: [outside-symlink: sh] + + sh "-c" "ln -s \"$BASEDIR\" '${BASEDIR}/OPAM/escapability-w-symlinks/lib/path'" (CWD=${BASEDIR}/OPAM/escapability-w-symlinks/.opam-switch/build/outside-symlink.1) +--> installed outside-symlink.1 +-Done. ++[ERROR] package outside-symlink attempted to install a file outside allowed destination directories: ${BASEDIR}/OPAM/escapability-w-symlinks/lib/path/escapist -> ${BASEDIR}/escapist. ++ ++.install file escaping allowed directories ++ ++ ++<><> Error report <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> +++- The following actions failed ++| - install outside-symlink 1 +++- ++- No changes have been performed ++'${OPAM} install outside-symlink -vv' failed. ++# Return code 31 # + ### : outside-symlink should not appear here + ### opam list -si outside-symlink +-outside-symlink + ### : No [escapist] file should appear in the test directory + ### ls $BASEDIR | grep escapist +-escapist + ### : ============= 2. Install in switch meta through symlink dir. ============ + ### : Our package install command creates a symlink inside the switch, pointing + ### : to the switch meta dir [.opam-switch]. +@@ -64,14 +72,22 @@ The following actions will be performed: + <><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><><><> + Processing 3/3: [meta-symlink: sh] + + sh "-c" "ln -s '${BASEDIR}/OPAM/escapability-w-symlinks/.opam-switch' '${BASEDIR}/OPAM/escapability-w-symlinks/lib/path2'" (CWD=${BASEDIR}/OPAM/escapability-w-symlinks/.opam-switch/build/meta-symlink.1) +--> installed meta-symlink.1 +-Done. ++[ERROR] package meta-symlink attempted to install a file outside allowed destination directories: ${BASEDIR}/OPAM/escapability-w-symlinks/lib/path2/escapist -> ${BASEDIR}/OPAM/escapability-w-symlinks/.opam-switch/escapist. ++ ++.install file escaping allowed directories ++ ++ ++<><> Error report <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> +++- The following actions failed ++| - install meta-symlink 1 +++- ++- No changes have been performed ++'${OPAM} install meta-symlink -vv' failed. ++# Return code 31 # + ### : meta-symlink should not appear here + ### opam list -si meta-symlink +-meta-symlink + ### : And the [to] file should not appear in the switch meta dir + ### ls $OPAMROOT/escapability-w-symlinks/.opam-switch | grep escapist +-escapist + ### : ============= 3. Install inside switch through symlink dir. ============ + ### : We also test that legitimate links are still permitted, i.e. + ### : links across opam dirs. diff -Nru opam-2.3.0/debian/patches/0007-Fix-OpamSystem.real_path.patch opam-2.3.0/debian/patches/0007-Fix-OpamSystem.real_path.patch --- opam-2.3.0/debian/patches/0007-Fix-OpamSystem.real_path.patch 1970-01-01 00:00:00.000000000 +0000 +++ opam-2.3.0/debian/patches/0007-Fix-OpamSystem.real_path.patch 2026-07-10 14:51:13.000000000 +0000 @@ -0,0 +1,94 @@ +From: Kate +Date: Tue, 7 Jul 2026 16:30:17 +0100 +Subject: Fix OpamSystem.real_path + +Paths were not completely resolved: when a part of the path is non +existent on disk, that part of path remained as is. +--- + master_changes.md | 1 + + src/core/opamCompat.ml | 14 +++++--------- + src/core/opamSystem.ml | 37 +++++++++++++++---------------------- + 3 files changed, 21 insertions(+), 31 deletions(-) + +diff --git a/master_changes.md b/master_changes.md +index a320bcf..b59e2be 100644 +--- a/master_changes.md ++++ b/master_changes.md +@@ -130,3 +130,4 @@ users) + * `OpamFilename.{,dir_}starts_with`: Fix a bug where `foo/bar` would be considered a prefix of `foo/bar-baz` [#6953 @NathanReb - fix #6948] + * `OpamFilename.{,dir_}starts_with`: `/` and `\` are now equivalent on Windows [#6953 @NathanReb] + * `OpamFilename.starts_with`: `starts_with "a/b" "a/b"` no longer returns `true` [#6953 @NathanReb] ++ * `OpamSystem.real_path`: fix a bug where paths after a non existent directory where not resolve [#7011 @kit-ty-kate - fix #7010] +diff --git a/src/core/opamCompat.ml b/src/core/opamCompat.ml +index 70a6686..1036d88 100644 +--- a/src/core/opamCompat.ml ++++ b/src/core/opamCompat.ml +@@ -53,15 +53,11 @@ module Unix = struct + + (** NOTE: OCaml >= 4.13 *) + let realpath s = +- let getchdir s = +- let p = +- try Sys.getcwd () +- with Sys_error _ -> Filename.get_temp_dir_name () +- in +- Unix.chdir s; +- p +- in +- try getchdir (getchdir s) with Unix.Unix_error _ -> s ++ let orig_cwd = Sys.getcwd () in ++ Unix.chdir s; ++ let r = Unix.getcwd () in ++ Sys.chdir orig_cwd; ++ r + + include Unix + end +diff --git a/src/core/opamSystem.ml b/src/core/opamSystem.ml +index 75dbb58..c420ab3 100644 +--- a/src/core/opamSystem.ml ++++ b/src/core/opamSystem.ml +@@ -55,28 +55,21 @@ let file_or_symlink_exists f = + + let (/) = Filename.concat + +-let real_path p = +- (* if Filename.is_relative p then *) +- match (try Some (Sys.is_directory p) with Sys_error _ -> None) with +- | None -> +- let rec resolve dir = +- if Sys.file_exists dir then OpamCompat.Unix.realpath dir else +- let parent = Filename.dirname dir in +- if dir = parent then dir +- else Filename.concat (resolve parent) (Filename.basename dir) +- in +- let p = +- if Filename.is_relative p then Filename.concat (Sys.getcwd ()) p +- else p +- in +- resolve p +- | Some true -> OpamCompat.Unix.realpath p +- | Some false -> +- let dir = OpamCompat.Unix.realpath (Filename.dirname p) in +- match Filename.basename p with +- | "." -> dir +- | base -> dir / base +- (* else p *) ++let rec real_path p = ++ try OpamCompat.Unix.realpath p with ++ | Unix.Unix_error _ when p = "." -> ++ raise (Sys_error "No such file or directory") ++ | Unix.Unix_error _ -> ++ let dirname = Filename.dirname p in ++ match Filename.basename p with ++ | "." -> real_path dirname ++ | ".." -> ++ let dirname = real_path dirname in ++ if String.equal (Filename.dirname dirname) dirname then ++ raise (Sys_error "Invalid path") ++ else ++ Filename.dirname dirname ++ | x -> real_path dirname / x + + let temp_basename prefix = + Printf.sprintf "%s-%d-%06x" prefix (OpamStubs.getpid ()) (Random.int 0xFFFFFF) diff -Nru opam-2.3.0/debian/patches/0008-add-unit-test-for-OpamSystem.real_path.patch opam-2.3.0/debian/patches/0008-add-unit-test-for-OpamSystem.real_path.patch --- opam-2.3.0/debian/patches/0008-add-unit-test-for-OpamSystem.real_path.patch 1970-01-01 00:00:00.000000000 +0000 +++ opam-2.3.0/debian/patches/0008-add-unit-test-for-OpamSystem.real_path.patch 2026-07-10 14:51:13.000000000 +0000 @@ -0,0 +1,247 @@ +From: Raja Boujbel +Date: Tue, 7 Jul 2026 18:50:15 +0200 +Subject: add unit test for OpamSystem.real_path + +--- + master_changes.md | 1 + + tests/lib/core/testOpamCore.ml | 7 +- + tests/lib/core/testOpamSystem.ml | 197 +++++++++++++++++++++ + .../{testOpamFilename.mli => testOpamSystem.mli} | 0 + 4 files changed, 202 insertions(+), 3 deletions(-) + create mode 100644 tests/lib/core/testOpamSystem.ml + copy tests/lib/core/{testOpamFilename.mli => testOpamSystem.mli} (100%) + +diff --git a/master_changes.md b/master_changes.md +index b59e2be..aef519c 100644 +--- a/master_changes.md ++++ b/master_changes.md +@@ -99,6 +99,7 @@ users) + ## Test + * Add `opamUnit` as a basic unit test framework [#6953 @NathanReb] + * Add unit tests for `OpamFilename.starts_with` and `dir_starts_with` in `tests/lib/core` [#6953 @NathanReb] ++ * Add unit test for `OpamSystem.real_path` [#7011 @rjbou] + + ## Benchmarks + +diff --git a/tests/lib/core/testOpamCore.ml b/tests/lib/core/testOpamCore.ml +index 11770b7..7af1f53 100644 +--- a/tests/lib/core/testOpamCore.ml ++++ b/tests/lib/core/testOpamCore.ml +@@ -9,6 +9,7 @@ + (**************************************************************************) + + let () = +- OpamUnit.run_tests +- (fun ctxt -> +- TestOpamFilename.test ~ctxt ()) ++ OpamUnit.run_tests @@ fun ctxt -> ++ TestOpamFilename.test ~ctxt (); ++ TestOpamSystem.test ~ctxt (); ++ () +diff --git a/tests/lib/core/testOpamSystem.ml b/tests/lib/core/testOpamSystem.ml +new file mode 100644 +index 0000000..128b29d +--- /dev/null ++++ b/tests/lib/core/testOpamSystem.ml +@@ -0,0 +1,197 @@ ++(**************************************************************************) ++(* *) ++(* Copyright 2026 OCamlPro *) ++(* *) ++(* All rights reserved. This file is distributed under the terms of the *) ++(* GNU Lesser General Public License version 2.1, with the special *) ++(* exception on linking described in the file LICENSE. *) ++(* *) ++(**************************************************************************) ++ ++ ++let (/) = Filename.concat ++ ++(* this is a copy from run.ml & opamSystem *) ++let finally f x k = match f x with ++ | r -> k (); r ++ | exception e -> ++ (* When we're at least 4.05+ ++ let bt = Printexc.get_raw_backtrace () in*) ++ (try k () with _ -> ()); ++ (*Printexc.raise_with_backtrace e bt*) ++ raise e ++ ++let rec mkdir_p dir = ++ if Sys.file_exists dir then () ++ else let () = mkdir_p (Filename.dirname dir) in ++ if not (Sys.file_exists dir) then ++ Unix.mkdir dir 0o777 ++ else () ++ ++let erase_file path = ++ try Sys.remove path ++ with Sys_error _ when Sys.win32 -> ++ (* Deal with read-only attribute on Windows. Ignore any error from chmod ++ so that the message always come from Sys.remove *) ++ let () = try Unix.chmod path 0o666 with Sys_error _ -> () in ++ Sys.remove path ++ ++let is_reg_dir dir = ++ match Unix.lstat dir with ++ | {Unix.st_kind = Unix.S_DIR; _} -> true ++ | {Unix.st_kind = ++ Unix.(S_REG | S_LNK | S_CHR | S_BLK | S_FIFO | S_SOCK); _} -> ++ false ++ | exception Unix.(Unix_error (ENOENT, _, _)) when Sys.win32 -> ++ (* This is usually caused by invalid symlinks extracted by a ++ Cygwin/MSYS2 tar. *) ++ false ++ ++let rm_rf path = ++ let rec erase path = ++ if is_reg_dir path then begin ++ Array.iter (fun entry -> erase (Filename.concat path entry)) ++ (Sys.readdir path); ++ Unix.rmdir path ++ end else erase_file path ++ in ++ try if Sys.file_exists path then erase path ++ with Sys_error err -> ++ raise (Sys_error (Printf.sprintf "Failed to remove %S (%s)" path err)) ++ ++let tmp = Unix.realpath (Filename.get_temp_dir_name ()) ++ ++let rec with_temp_dir f = ++ let s = ++ tmp / Printf.sprintf "opam-reftest-%06x" (Random.int 0xffffff) ++ in ++ if Sys.file_exists s then ++ with_temp_dir f ++ else ++ (mkdir_p s; ++ finally f s @@ fun () -> ++ rm_rf s) ++ ++let touch file = ++ let oc = open_out_bin file in ++ output_string oc ""; ++ close_out oc ++(* -- end copy *) ++ ++let test_real_path ~ctxt () = ++ let fun_name = "OpamSystem.real_path" in ++ let test ~on ~setup ~unresolved ~expected = ++ let test_name = Printf.sprintf "(%s)" unresolved in ++ if List.exists (String.equal Sys.os_type) on then ++ (Option.iter (fun f -> f ()) setup; ++ let got = ++ try Ok (OpamSystem.real_path unresolved) ++ with Sys_error msg -> Error msg ++ | e -> Error (Printexc.to_string e) ++ in ++ if Result.equal ~ok:String.equal ~error:String.equal expected got then ++ OpamUnit.success ~ctxt ~fun_name ~test_name () ++ else ++ let expected = ++ match expected with ++ | Ok x -> x ++ | Error msg -> "Error:"^msg ++ in ++ let got = match got with Ok x -> x | Error msg -> "Error:"^msg in ++ OpamUnit.failure ~ctxt ~fun_name ~test_name ~expected ~got ()) ++ else ++ let reason = Printf.sprintf "Test does not run on %s" Sys.os_type in ++ OpamUnit.skip ~ctxt ~fun_name ~test_name ~reason () ++ in ++ with_temp_dir @@ fun dir -> ++ let d1 = dir / "d1" in ++ let d2 = d1 / "d2" in ++ let d3 = d2 / "d3" in ++ mkdir_p d3; ++ let f2 = d2 / "f2" in ++ touch f2; ++ let orig_cwd = Sys.getcwd () in ++ Sys.chdir dir; ++ let (!) p = dir / p in ++ let testcases = ++ let win32 = ["Win32"] in ++ let unix = ["Unix"] in ++ let cygwin = ["Cygwin"] in ++ let all = unix @ cygwin @ win32 in ++ let absolute_paths = ++ let existent = [ ++ all, None, ! "d1"/".", Ok (! "d1"); ++ all, None, ! "d1"/"."/"."/"."/"", Ok (! "d1"); ++ all, None, ! "d1", Ok (! "d1"); ++ all, None, ! "d1"/"..", Ok dir; ++ all, None, ! "d1"/".."/"", Ok dir; ++ all, None, ! "d1"/"d2", Ok (! "d1"/"d2"); ++ all, None, ! "d1"/".."/"d1", Ok (! "d1"); ++ all, None, ! "d1"/"d2"/"d3"/".."/"f2", Ok (! "d1"/"d2"/"f2"); ++ ] in ++ let inexistent = [ ++ all, None, ! "d1"/"dx", Ok (! "d1"/"dx"); ++ all, None, ! "d1"/".."/"dx", Ok (! "dx"); ++ all, None, ! "d1"/"dx"/"d3"/".."/"fx", Ok (! "d1"/"dx"/"fx"); ++ ] ++ in ++ existent @ inexistent ++ in ++ let relative_paths = ++ let existent = [ ++ all, None, ".", Ok dir; ++ all, None, "."/"."/"."/"", Ok dir; ++ all, None, "..", Ok tmp; ++ all, None, "d1"/".", Ok (! "d1"); ++ all, None, "d1"/"."/"."/"."/"", Ok (! "d1"); ++ all, None, "d1", Ok (! "d1"); ++ all, None, "d1"/"..", Ok dir; ++ all, None, "d1"/".."/"", Ok dir; ++ all, None, "d1"/"d2", Ok (! "d1"/"d2"); ++ all, None, "d1"/".."/"d1", Ok (! "d1"); ++ all, None, "d1"/"d2"/"d3"/".."/"f2", Ok (! "d1"/"d2"/"f2"); ++ win32, None, "C:Windows", Ok (! "Windows"); ++ ] in ++ let inexistent = [ ++ all, None, "d1"/"dx", Ok (! "d1"/"dx"); ++ all, None, "d1"/".."/"dx", Ok (! "dx"); ++ all, None, "d1"/"dx"/"d3"/".."/"fx", Ok (! "d1"/"dx"/"fx"); ++ win32, None, "C:/d1/d2", Ok {|C:\d1\d2|}; ++ win32, None, {|C:\d1\d2\..|}, Ok {|C:\d1|}; ++ ] ++ in ++ existent @ inexistent ++ in ++ let always_root = [ ++ unix @ cygwin, None, ".."/".."/".."/".."/".."/".."/".."/".."/".."/".."/".."/".."/".."/".."/"..", Ok "/"; ++ unix @ cygwin, None, "/..", Ok "/"; ++ win32, None, ".."/".."/".."/".."/".."/".."/".."/".."/".."/".."/".."/".."/".."/".."/"..", Ok {|C:\|}; ++ win32, None, "/..", Ok {|C:\|}; ++ win32, None, "C:/../../../", Ok {|C:\|}; ++ ] in ++ (* keep last because of chdirs *) ++ let remove_dir = ++ let create_go_rm d = ++ Some (fun () -> mkdir_p d; Sys.chdir d; rm_rf d) ++ in ++ let d4 = d1/"d4" in ++ [ ++ unix, create_go_rm d4 , ".", Error "No such file or directory"; ++ unix, create_go_rm d4 , d4, Ok d4; ++ unix, create_go_rm d4 , ".."/"d1", Error "No such file or directory"; ++ unix, create_go_rm d4 , "d4"/".."/"d1", Error "No such file or directory"; ++ ] ++ in ++ absolute_paths ++ @ relative_paths ++ @ always_root ++ @ remove_dir ++ in ++ List.iter ++ (fun (on, setup, unresolved, expected) -> ++ test ~on ~setup ~unresolved ~expected) ++ testcases; ++ Sys.chdir orig_cwd ++ ++let test ~ctxt () = ++ test_real_path ~ctxt () +diff --git a/tests/lib/core/testOpamFilename.mli b/tests/lib/core/testOpamSystem.mli +similarity index 100% +copy from tests/lib/core/testOpamFilename.mli +copy to tests/lib/core/testOpamSystem.mli diff -Nru opam-2.3.0/debian/patches/0009-Re-allow-.install-files-containing.patch opam-2.3.0/debian/patches/0009-Re-allow-.install-files-containing.patch --- opam-2.3.0/debian/patches/0009-Re-allow-.install-files-containing.patch 1970-01-01 00:00:00.000000000 +0000 +++ opam-2.3.0/debian/patches/0009-Re-allow-.install-files-containing.patch 2026-07-10 14:51:13.000000000 +0000 @@ -0,0 +1,40 @@ +From: Kate +Date: Mon, 6 Jul 2026 21:21:25 +0100 +Subject: Re-allow .install files containing '..' + +Partially reverts ae877c0f74f696e05c2d524913c682c8aeaad918 + +This check is no longer necessary since d7f7d8e39afcf8ade6b560224e5b084a924f2b15 +and broke opam repositories providing cross-compiled packages +--- + master_changes.md | 1 + + src/format/opamFile.ml | 4 +--- + 2 files changed, 2 insertions(+), 3 deletions(-) + +diff --git a/master_changes.md b/master_changes.md +index aef519c..cf98851 100644 +--- a/master_changes.md ++++ b/master_changes.md +@@ -114,6 +114,7 @@ users) + ## Doc + + ## Security fixes ++ * Invalidate .install fields containing absolute destination filepath except when in the `misc` field [#6897 #7008 @kit-ty-kate] + * Fix a bug that allowed a package to install files anywhere on the system using a symlink to an external directory without warning the user and asking for their permission: CVE-2026-57825. [#7005 @NathanReb] + + # API updates +diff --git a/src/format/opamFile.ml b/src/format/opamFile.ml +index c0b4774..7539636 100644 +--- a/src/format/opamFile.ml ++++ b/src/format/opamFile.ml +@@ -3807,9 +3807,7 @@ module Dot_installSyntax = struct + (Pp.opt @@ + Pp.singleton -| Pp.V.string -| Pp.pp ~name:"rel-filename" + (fun ~pos s -> +- if OpamFilename.might_escape ~sep:`Unspecified s then +- Pp.bad_format ~pos "%s references its parent directory." s +- else if Filename.is_relative s then ++ if Filename.is_relative s then + OpamFilename.Base.of_string s + else + Pp.bad_format ~pos "%s is an absolute filename." s) diff -Nru opam-2.3.0/debian/patches/0010-Stop-the-installation-process-via-.install-file-from.patch opam-2.3.0/debian/patches/0010-Stop-the-installation-process-via-.install-file-from.patch --- opam-2.3.0/debian/patches/0010-Stop-the-installation-process-via-.install-file-from.patch 1970-01-01 00:00:00.000000000 +0000 +++ opam-2.3.0/debian/patches/0010-Stop-the-installation-process-via-.install-file-from.patch 2026-07-10 14:51:13.000000000 +0000 @@ -0,0 +1,69 @@ +From: Kate +Date: Wed, 8 Jul 2026 16:26:50 +0100 +Subject: Stop the installation process via .install file from creating + unnecessary intermediate directories for paths that contain '..' + +--- + master_changes.md | 1 + + src/client/opamAction.ml | 18 +++++++++--------- + 2 files changed, 10 insertions(+), 9 deletions(-) + +diff --git a/master_changes.md b/master_changes.md +index cf98851..d2a7f42 100644 +--- a/master_changes.md ++++ b/master_changes.md +@@ -21,6 +21,7 @@ users) + ## Config report + + ## Actions ++ * Stop the installation process via .install file from creating unnecessary intermediate directories for paths that contain `..` [#7015 @kit-ty-kate] + + ## Install + +diff --git a/src/client/opamAction.ml b/src/client/opamAction.ml +index 9effbba..5cfdb70 100644 +--- a/src/client/opamAction.ml ++++ b/src/client/opamAction.ml +@@ -101,7 +101,7 @@ let preprocess_dot_install_t st nv build_dir = + else + (base, false) in + let src_file = OpamFilename.create build_dir base.c in +- let dst_file = match dst with ++ let unsafe_dst_file = match dst with + | None -> OpamFilename.create dst_dir (OpamFilename.basename src_file) + | Some d -> + if append && not (OpamFilename.Base.check_suffix d ".exe") then +@@ -109,25 +109,25 @@ let preprocess_dot_install_t st nv build_dir = + (OpamFilename.Base.add_extension d "exe") + else + OpamFilename.create dst_dir d in ++ let dst_file = ++ OpamFilename.of_string ++ (OpamSystem.real_path (OpamFilename.to_string unsafe_dst_file)) ++ in + let file = file_wo_prefix dst_file in + let inst warning = + if append then warning (OpamFilename.to_string src_file) `Add_exe; + let check, warn = check ~src:build_dir ~dst:dst_dir base in +- let real_dst = +- OpamFilename.of_string +- (OpamSystem.real_path (OpamFilename.to_string dst_file)) +- in + let escapes = +- not (OpamFilename.starts_with switch_prefix real_dst) +- || (OpamFilename.starts_with switch_meta real_dst) ++ not (OpamFilename.starts_with switch_prefix dst_file) ++ || (OpamFilename.starts_with switch_meta dst_file) + in + if escapes then + (OpamConsole.error + "package %s attempted to install a file outside allowed \ + destination directories: %s -> %s." + (OpamPackage.Name.to_string name) +- (OpamFilename.to_string dst_file) +- (OpamFilename.to_string real_dst); ++ (OpamFilename.to_string unsafe_dst_file) ++ (OpamFilename.to_string dst_file); + failwith ".install file escaping allowed directories") + else if check then + OpamFilename.install ~warning ~exec ~src:src_file ~dst:dst_file (); diff -Nru opam-2.3.0/debian/patches/series opam-2.3.0/debian/patches/series --- opam-2.3.0/debian/patches/series 2026-04-16 13:47:56.000000000 +0000 +++ opam-2.3.0/debian/patches/series 2026-07-10 14:51:13.000000000 +0000 @@ -1 +1,10 @@ 0001-Invalidate-.install-fields-containing-destination-fi.patch +0002-Add-tiny-Ounit-like-test-framework.patch +0003-Add-unit-tests-for-OpamFilename.starts_with-like-fun.patch +0004-Fix-OpamFilename.starts_with-and-dir_starts_with.patch +0005-Add-a-test-ensuring-installing-files-through-a-.inst.patch +0006-Fix-bypass-switch-prefix-check-on-install-using-syml.patch +0007-Fix-OpamSystem.real_path.patch +0008-add-unit-test-for-OpamSystem.real_path.patch +0009-Re-allow-.install-files-containing.patch +0010-Stop-the-installation-process-via-.install-file-from.patch