Skip to content

Commit 10325a9

Browse files
l46983284-cpugiampaologithub-actions[bot]
authored
[Windows] disk_partitions: use wide-char APIs to fix UnicodeDecodeError (#1959) (#2975)
Fixes #1959. On Windows, `psutil.disk_partitions()` crashed with `UnicodeDecodeError` on systems whose ANSI code page is not UTF-8 (e.g. cp1251): the ANSI variants of `GetLogicalDriveStrings`, `FindFirstVolumeMountPoint` and `GetVolumeInformation` return bytes in the system ANSI code page, and the `(ssss)` tuple format decodes them as strict UTF-8. Following the maintainer's direction in the issue (use `GetVolumeInformationW`), this switches the whole function family to wide-char APIs: `GetLogicalDriveStringsW`, `GetDriveTypeW`, `GetVolumeInformationW`, `FindFirstVolumeMountPointW`/`FindNextVolumeMountPointW`, building strings via `PyUnicode_FromWideChar`. Widening `GetVolumeInformation` alone would not have been enough: the actual crash vector in the issue (byte 0xc5 in position 4 of a mount-point path under cp1251) comes from `FindFirstVolumeMountPoint`. `psutil_QueryDosDevice` is converted to `QueryDosDeviceW` as well — it had the same class of mismatch in reverse: the input path was UTF-8-decoded while `QueryDosDevice` returns ANSI bytes, so device targets with non-ASCII characters could never match. Adds a regression test asserting the unicode contract of all four tuple fields (the original crash requires a live non-UTF8 ANSI code page and cannot be reproduced from Python on en-US CI). --------- Signed-off-by: Alex Chen <l46983284@gmail.com> Co-authored-by: Giampaolo Rodola <g.rodola@gmail.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 76bb19f commit 10325a9

3 files changed

Lines changed: 93 additions & 46 deletions

File tree

docs/changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,9 @@ Reorganization of process memory APIs (:gh:`2731`, :gh:`2736`, :gh:`2723`,
368368
via ``NtQuerySystemInformation(SystemTimeOfDayInformation)``, replacing the
369369
old ``time.time() - uptime()`` computation that sampled two counters from
370370
Python and produced sub-second differences.
371+
- :gh:`1959`, [Windows]: :func:`disk_partitions` could raise
372+
``UnicodeDecodeError`` on systems whose ANSI code page is not UTF-8 (e.g.
373+
cp1251). It now uses the wide-char Windows APIs throughout.
371374
- :gh:`2383`, [Windows]: :meth:`WindowsService.description` may fail with
372375
``ERROR_FILE_NOT_FOUND`` when the description points at a missing resource
373376
(e.g. ``WaaSMedicSvc``), which also broke :meth:`WindowsService.as_dict`. Now

docs/credits.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Code contributors by year
109109
2026
110110
~~~~
111111

112-
* :user:`Alex Chen <l46983284-cpu>` - :gh:`2859`
112+
* :user:`Alex Chen <l46983284-cpu>` - :gh:`2859`, :gh:`1959`
113113
* :user:`Amaan Qureshi <amaanq>` - :gh:`2770`
114114
* :user:`Anshul Nautiyal <ANSHUL-REAL>` - :gh:`2858`
115115
* :user:`Arman Luthra <Arman-Luthra>` - :gh:`2695`

psutil/arch/windows/disk.c

Lines changed: 89 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
#include <Python.h>
88
#include <windows.h>
9-
#include <tchar.h>
109

1110
#include "../../arch/all/init.h"
1211

@@ -221,21 +220,25 @@ psutil_disk_io_counters(PyObject *self, PyObject *args) {
221220

222221
PyObject *
223222
psutil_disk_partitions(PyObject *self, PyObject *args) {
224-
DWORD num_bytes;
225-
char drive_strings[255];
226-
char *drive_letter = drive_strings;
227-
char mp_buf[MAX_PATH];
228-
char mp_path[MAX_PATH];
223+
DWORD num_chars;
224+
wchar_t drive_strings[255];
225+
wchar_t *drive_letter = drive_strings;
226+
wchar_t mp_buf[MAX_PATH];
227+
wchar_t mp_path[MAX_PATH];
229228
int all = 0;
230229
int type;
231230
int ret;
232231
unsigned int old_mode = 0;
233232
char opts[50];
234233
HANDLE mp_h;
235234
BOOL mp_flag = TRUE;
236-
char fs_type[MAX_PATH + 1] = {0};
235+
wchar_t fs_type[MAX_PATH + 1] = {0};
237236
DWORD pflags = 0;
238237
DWORD lpMaximumComponentLength = 0; // max file name
238+
PyObject *py_drive = NULL;
239+
PyObject *py_mount = NULL;
240+
PyObject *py_fs = NULL;
241+
PyObject *py_opts = NULL;
239242
PyObject *py_retlist = PyList_New(0);
240243

241244
if (py_retlist == NULL) {
@@ -250,11 +253,11 @@ psutil_disk_partitions(PyObject *self, PyObject *args) {
250253
goto error;
251254

252255
Py_BEGIN_ALLOW_THREADS
253-
num_bytes = GetLogicalDriveStrings(254, drive_letter);
256+
num_chars = GetLogicalDriveStringsW(254, drive_letter);
254257
Py_END_ALLOW_THREADS
255258

256-
if (num_bytes == 0) {
257-
psutil_oserror();
259+
if (num_chars == 0) {
260+
psutil_oserror_wsyscall("GetLogicalDriveStringsW");
258261
goto error;
259262
}
260263

@@ -263,7 +266,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) {
263266
fs_type[0] = 0;
264267

265268
Py_BEGIN_ALLOW_THREADS
266-
type = GetDriveType(drive_letter);
269+
type = GetDriveTypeW(drive_letter);
267270
Py_END_ALLOW_THREADS
268271

269272
// by default we only show hard drives and cd-roms
@@ -276,7 +279,7 @@ psutil_disk_partitions(PyObject *self, PyObject *args) {
276279
// floppy disk: skip it by default as it introduces a
277280
// considerable slowdown.
278281
if ((type == DRIVE_REMOVABLE)
279-
&& (strcmp(drive_letter, "A:\\") == 0))
282+
&& (wcscmp(drive_letter, L"A:\\") == 0))
280283
{
281284
goto next;
282285
}
@@ -285,8 +288,8 @@ psutil_disk_partitions(PyObject *self, PyObject *args) {
285288
// May spin up a removable drive or go over the wire for a
286289
// network one, so do it without the GIL.
287290
Py_BEGIN_ALLOW_THREADS
288-
ret = GetVolumeInformation(
289-
(LPCTSTR)drive_letter,
291+
ret = GetVolumeInformationW(
292+
drive_letter,
290293
NULL, // we don't want the volume name
291294
0,
292295
NULL,
@@ -320,40 +323,64 @@ psutil_disk_partitions(PyObject *self, PyObject *args) {
320323
str_append(opts, sizeof(opts), ",");
321324
str_append(opts, sizeof(opts), psutil_get_drive_type(type));
322325

326+
// Convert the strings which will go into the result tuples.
327+
py_drive = PyUnicode_FromWideChar(drive_letter, wcslen(drive_letter));
328+
if (py_drive == NULL)
329+
goto error;
330+
py_fs = PyUnicode_FromWideChar(fs_type, wcslen(fs_type));
331+
if (py_fs == NULL)
332+
goto error;
333+
// opts holds pure ASCII, so plain UTF-8 decoding is safe.
334+
py_opts = PyUnicode_FromString(opts);
335+
if (py_opts == NULL)
336+
goto error;
337+
323338
// Check for mount points on this volume and add/get info
324339
// (checks first to know if we can even have mount points)
325340
if ((ret != 0) && (pflags & FILE_SUPPORTS_REPARSE_POINTS)) {
326341
Py_BEGIN_ALLOW_THREADS
327-
mp_h = FindFirstVolumeMountPoint(drive_letter, mp_buf, MAX_PATH);
342+
mp_h = FindFirstVolumeMountPointW(drive_letter, mp_buf, MAX_PATH);
328343
Py_END_ALLOW_THREADS
329344

330345
if (mp_h != INVALID_HANDLE_VALUE) {
331346
mp_flag = TRUE;
332347
while (mp_flag) {
333-
// Append full mount path with drive letter
334-
str_copy(
335-
mp_path, sizeof(mp_path), drive_letter
336-
); // initialize
337-
str_append(
338-
mp_path, sizeof(mp_path), mp_buf
339-
); // append mount point
348+
// Append full mount path with drive letter.
349+
mp_path[0] = L'\0';
350+
wcsncat(mp_path, drive_letter, _ARRAYSIZE(mp_path) - 1);
351+
wcsncat(
352+
mp_path,
353+
mp_buf,
354+
_ARRAYSIZE(mp_path) - wcslen(mp_path) - 1
355+
);
356+
357+
py_mount = PyUnicode_FromWideChar(
358+
mp_path, wcslen(mp_path)
359+
);
360+
if (py_mount == NULL) {
361+
FindVolumeMountPointClose(mp_h);
362+
goto error;
363+
}
340364

341365
if (!pylist_append_fmt(
342366
py_retlist,
343-
"(ssss)",
344-
drive_letter,
345-
mp_path,
346-
fs_type, // typically "NTFS"
347-
opts
367+
"(OOOO)",
368+
py_drive,
369+
py_mount,
370+
py_fs, // typically "NTFS"
371+
py_opts
348372
))
349373
{
350374
FindVolumeMountPointClose(mp_h);
351375
goto error;
352376
}
377+
Py_CLEAR(py_mount);
353378

354379
// Continue looking for more mount points
355380
Py_BEGIN_ALLOW_THREADS
356-
mp_flag = FindNextVolumeMountPoint(mp_h, mp_buf, MAX_PATH);
381+
mp_flag = FindNextVolumeMountPointW(
382+
mp_h, mp_buf, MAX_PATH
383+
);
357384
Py_END_ALLOW_THREADS
358385
}
359386
FindVolumeMountPointClose(mp_h);
@@ -362,25 +389,33 @@ psutil_disk_partitions(PyObject *self, PyObject *args) {
362389

363390
if (!pylist_append_fmt(
364391
py_retlist,
365-
"(ssss)",
366-
drive_letter,
367-
drive_letter,
368-
fs_type, // either FAT, FAT32, NTFS, HPFS, CDFS, UDF or NWFS
369-
opts
392+
"(OOOO)",
393+
py_drive,
394+
py_drive,
395+
py_fs, // either FAT, FAT32, NTFS, HPFS, CDFS, UDF or NWFS
396+
py_opts
370397
))
371398
{
372399
goto error;
373400
}
374401
goto next;
375402

376403
next:
377-
drive_letter = strchr(drive_letter, 0) + 1;
404+
Py_CLEAR(py_opts);
405+
Py_CLEAR(py_fs);
406+
Py_CLEAR(py_mount);
407+
Py_CLEAR(py_drive);
408+
drive_letter = wcschr(drive_letter, 0) + 1;
378409
}
379410

380411
SetErrorMode(old_mode);
381412
return py_retlist;
382413

383414
error:
415+
Py_XDECREF(py_opts);
416+
Py_XDECREF(py_fs);
417+
Py_XDECREF(py_mount);
418+
Py_XDECREF(py_drive);
384419
SetErrorMode(old_mode);
385420
Py_DECREF(py_retlist);
386421
return NULL;
@@ -392,23 +427,32 @@ psutil_disk_partitions(PyObject *self, PyObject *args) {
392427
// If no match is found return an empty string.
393428
PyObject *
394429
psutil_QueryDosDevice(PyObject *self, PyObject *args) {
395-
LPCTSTR lpDevicePath;
396-
TCHAR d = TEXT('A');
397-
TCHAR szBuff[5];
430+
PyObject *py_device_path;
431+
PyObject *py_ret = NULL;
432+
wchar_t *device_path;
433+
wchar_t d = L'A';
398434

399-
if (!PyArg_ParseTuple(args, "s", &lpDevicePath))
435+
if (!PyArg_ParseTuple(args, "U", &py_device_path))
400436
return NULL;
401437

402-
while (d <= TEXT('Z')) {
403-
TCHAR szDeviceName[3] = {d, TEXT(':'), TEXT('\0')};
404-
TCHAR szTarget[512] = {0};
405-
if (QueryDosDevice(szDeviceName, szTarget, 511) != 0) {
406-
if (_tcscmp(lpDevicePath, szTarget) == 0) {
407-
_stprintf_s(szBuff, _countof(szBuff), TEXT("%c:"), d);
408-
return PyUnicode_FromString(szBuff);
438+
device_path = PyUnicode_AsWideCharString(py_device_path, NULL);
439+
if (device_path == NULL)
440+
return NULL;
441+
442+
while (d <= L'Z') {
443+
wchar_t szDeviceName[3] = {d, L':', L'\0'};
444+
wchar_t szTarget[512] = {0};
445+
if (QueryDosDeviceW(szDeviceName, szTarget, 511) != 0) {
446+
if (wcscmp(device_path, szTarget) == 0) {
447+
py_ret = PyUnicode_FromWideChar(
448+
szDeviceName, wcslen(szDeviceName)
449+
);
450+
PyMem_Free(device_path);
451+
return py_ret;
409452
}
410453
}
411454
d++;
412455
}
456+
PyMem_Free(device_path);
413457
return PyUnicode_FromString("");
414458
}

0 commit comments

Comments
 (0)