Skip to content

Commit cc6d625

Browse files
committed
pacman: reject Windows case-folding collisions
1 parent a702027 commit cc6d625

4 files changed

Lines changed: 143 additions & 2 deletions

File tree

cmake/PacmanClient.cmake

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ function(vitasdk_add_pacman_client deps_dir install_dir zlib_target)
2828
"${PROJECT_SOURCE_DIR}/patches/pacman/0001-allow-writable-non-root-installation-roots.patch|1"
2929
"${PROJECT_SOURCE_DIR}/patches/pacman/0002-embed-libalpm-in-static-clients.patch|1"
3030
"${PROJECT_SOURCE_DIR}/patches/pacman/0003-build-libalpm-with-mingw.patch|1"
31-
"${PROJECT_SOURCE_DIR}/patches/pacman/0004-initialize-locale-without-i18n.patch|1")
31+
"${PROJECT_SOURCE_DIR}/patches/pacman/0004-initialize-locale-without-i18n.patch|1"
32+
"${PROJECT_SOURCE_DIR}/patches/pacman/0005-reject-windows-casefold-collisions.patch|1")
3233
list(JOIN pacman_patch_series "^" pacman_patch_series_arg)
3334

3435
set(common_cmake_args
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
Reject package paths that collide on case-insensitive Windows filesystems.
2+
3+
Libalpm compares package paths byte-for-byte. On Windows, two distinct archive
4+
members such as Collision.h and collision.h therefore pass the conflict check
5+
but address the same file; extraction silently lets one replace the other.
6+
7+
For Windows builds, convert each UTF-8 package path to UTF-16 once, sort a
8+
temporary view with CompareStringOrdinal's case-insensitive ordering, and
9+
reject adjacent equivalents before the package enters a transaction. This
10+
uses Windows' own Unicode comparison rather than an ASCII-only approximation.
11+
12+
Based on pacman 7.1.0 commit 5683f8477a0afcc6b331766175a83445b2dcfe89.
13+
14+
--- a/lib/libalpm/be_package.c
15+
+++ b/lib/libalpm/be_package.c
16+
@@ -25,6 +25,9 @@
17+
#include <sys/stat.h>
18+
#include <fcntl.h>
19+
#include <limits.h>
20+
+#if defined(_WIN32)
21+
+#include <windows.h>
22+
+#endif
23+
24+
/* libarchive */
25+
#include <archive.h>
26+
@@ -47,6 +50,92 @@ struct package_changelog {
27+
int fd;
28+
};
29+
30+
+#if defined(_WIN32)
31+
+struct casefold_path {
32+
+ const char *utf8;
33+
+ wchar_t *wide;
34+
+};
35+
+
36+
+static int casefold_path_cmp(const void *left, const void *right)
37+
+{
38+
+ const struct casefold_path *a = left;
39+
+ const struct casefold_path *b = right;
40+
+ int result = CompareStringOrdinal(a->wide, -1, b->wide, -1, TRUE);
41+
+
42+
+ if(result == CSTR_LESS_THAN) {
43+
+ return -1;
44+
+ }
45+
+ if(result == CSTR_GREATER_THAN) {
46+
+ return 1;
47+
+ }
48+
+ return strcmp(a->utf8, b->utf8);
49+
+}
50+
+
51+
+static int filelist_has_casefold_collision(alpm_handle_t *handle,
52+
+ const alpm_filelist_t *filelist)
53+
+{
54+
+ struct casefold_path *paths;
55+
+ size_t i;
56+
+ int ret = 0;
57+
+
58+
+ if(filelist->count < 2) {
59+
+ return 0;
60+
+ }
61+
+ paths = calloc(filelist->count, sizeof(struct casefold_path));
62+
+ if(paths == NULL) {
63+
+ handle->pm_errno = ALPM_ERR_MEMORY;
64+
+ return -1;
65+
+ }
66+
+
67+
+ for(i = 0; i < filelist->count; ++i) {
68+
+ const char *name = filelist->files[i].name;
69+
+ int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
70+
+ name, -1, NULL, 0);
71+
+ paths[i].utf8 = name;
72+
+ if(length <= 0) {
73+
+ _alpm_log(handle, ALPM_LOG_ERROR,
74+
+ _("package path is not valid UTF-8: %s\n"), name);
75+
+ ret = 1;
76+
+ break;
77+
+ }
78+
+ paths[i].wide = calloc((size_t)length, sizeof(wchar_t));
79+
+ if(paths[i].wide == NULL) {
80+
+ handle->pm_errno = ALPM_ERR_MEMORY;
81+
+ ret = -1;
82+
+ break;
83+
+ }
84+
+ if(MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, name, -1,
85+
+ paths[i].wide, length) == 0) {
86+
+ _alpm_log(handle, ALPM_LOG_ERROR,
87+
+ _("package path is not valid UTF-8: %s\n"), name);
88+
+ ret = 1;
89+
+ break;
90+
+ }
91+
+ }
92+
+
93+
+ if(ret == 0) {
94+
+ qsort(paths, filelist->count, sizeof(struct casefold_path),
95+
+ casefold_path_cmp);
96+
+ for(i = 1; i < filelist->count; ++i) {
97+
+ if(CompareStringOrdinal(paths[i - 1].wide, -1,
98+
+ paths[i].wide, -1, TRUE) == CSTR_EQUAL) {
99+
+ _alpm_log(handle, ALPM_LOG_ERROR,
100+
+ _("package paths collide on Windows: %s and %s\n"),
101+
+ paths[i - 1].utf8, paths[i].utf8);
102+
+ ret = 1;
103+
+ break;
104+
+ }
105+
+ }
106+
+ }
107+
+
108+
+ for(i = 0; i < filelist->count; ++i) {
109+
+ free(paths[i].wide);
110+
+ }
111+
+ free(paths);
112+
+ return ret;
113+
+}
114+
+#endif
115+
+
116+
/**
117+
* Open a package changelog for reading. Similar to fopen in functionality,
118+
* except that the returned 'file stream' is from an archive.
119+
@@ -671,6 +760,15 @@ alpm_pkg_t *_alpm_pkg_load_internal(alpm_handle_t *handle,
120+
"sorting package filelist for %s\n", pkgfile);
121+
122+
_alpm_filelist_sort(&newpkg->files);
123+
+#if defined(_WIN32)
124+
+ ret = filelist_has_casefold_collision(handle, &newpkg->files);
125+
+ if(ret < 0) {
126+
+ goto error;
127+
+ }
128+
+ if(ret > 0) {
129+
+ goto pkg_invalid;
130+
+ }
131+
+#endif
132+
}
133+
newpkg->infolevel |= INFRQ_FILES;
134+
}

tests/pacman/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ it, the reduced `-Di18n=false` client stays in the ASCII C application locale
2323
and libarchive cannot read UTF-8 package paths, even though the MSYS runtime
2424
uses UTF-8 internally for Windows filenames.
2525

26+
The fifth patch rejects a Windows package before transaction preparation when
27+
two UTF-8 payload paths compare equal under Windows' case-insensitive Unicode
28+
ordering. This prevents distinct archive members from silently addressing and
29+
overwriting the same file on the SDK filesystem.
30+
2631
The initial macOS arm64 diagnostic build used Meson 1.5.2 with:
2732

2833
```sh

tests/pacman/msys-pacman-build.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ actual_revision=$(git -C "$source_directory" rev-parse HEAD)
3737
for patch in \
3838
"$repository_root/patches/pacman/0001-allow-writable-non-root-installation-roots.patch" \
3939
"$repository_root/patches/pacman/0002-embed-libalpm-in-static-clients.patch" \
40-
"$repository_root/patches/pacman/0004-initialize-locale-without-i18n.patch"
40+
"$repository_root/patches/pacman/0004-initialize-locale-without-i18n.patch" \
41+
"$repository_root/patches/pacman/0005-reject-windows-casefold-collisions.patch"
4142
do
4243
git -C "$source_directory" apply --check --whitespace=error-all "$patch"
4344
git -C "$source_directory" apply --whitespace=error-all "$patch"

0 commit comments

Comments
 (0)