Skip to content

Commit 4dc42c6

Browse files
dschogitster
authored andcommitted
mingw: refuse paths containing reserved names
There are a couple of reserved names that cannot be file names on Windows, such as `AUX`, `NUL`, etc. For an almost complete list, see https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file If one would try to create a directory named `NUL`, it would actually "succeed", i.e. the call would return success, but nothing would be created. Worse, even adding a file extension to the reserved name does not make it a valid file name. To understand the rationale behind that behavior, see https://devblogs.microsoft.com/oldnewthing/20031022-00/?p=42073 Let's just disallow them all. Signed-off-by: Johannes Schindelin <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 98d9b23 commit 4dc42c6

File tree

3 files changed

+110
-18
lines changed

3 files changed

+110
-18
lines changed

compat/mingw.c

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ int mingw_mkdir(const char *path, int mode)
393393
int ret;
394394
wchar_t wpath[MAX_PATH];
395395

396-
if (!is_valid_win32_path(path)) {
396+
if (!is_valid_win32_path(path, 0)) {
397397
errno = EINVAL;
398398
return -1;
399399
}
@@ -479,7 +479,7 @@ int mingw_open (const char *filename, int oflags, ...)
479479
mode = va_arg(args, int);
480480
va_end(args);
481481

482-
if (!is_valid_win32_path(filename)) {
482+
if (!is_valid_win32_path(filename, !create)) {
483483
errno = create ? EINVAL : ENOENT;
484484
return -1;
485485
}
@@ -550,14 +550,13 @@ FILE *mingw_fopen (const char *filename, const char *otype)
550550
int hide = needs_hiding(filename);
551551
FILE *file;
552552
wchar_t wfilename[MAX_PATH], wotype[4];
553-
if (!is_valid_win32_path(filename)) {
553+
if (filename && !strcmp(filename, "/dev/null"))
554+
wcscpy(wfilename, L"nul");
555+
else if (!is_valid_win32_path(filename, 1)) {
554556
int create = otype && strchr(otype, 'w');
555557
errno = create ? EINVAL : ENOENT;
556558
return NULL;
557-
}
558-
if (filename && !strcmp(filename, "/dev/null"))
559-
wcscpy(wfilename, L"nul");
560-
else if (xutftowcs_path(wfilename, filename) < 0)
559+
} else if (xutftowcs_path(wfilename, filename) < 0)
561560
return NULL;
562561

563562
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -580,14 +579,13 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
580579
int hide = needs_hiding(filename);
581580
FILE *file;
582581
wchar_t wfilename[MAX_PATH], wotype[4];
583-
if (!is_valid_win32_path(filename)) {
582+
if (filename && !strcmp(filename, "/dev/null"))
583+
wcscpy(wfilename, L"nul");
584+
else if (!is_valid_win32_path(filename, 1)) {
584585
int create = otype && strchr(otype, 'w');
585586
errno = create ? EINVAL : ENOENT;
586587
return NULL;
587-
}
588-
if (filename && !strcmp(filename, "/dev/null"))
589-
wcscpy(wfilename, L"nul");
590-
else if (xutftowcs_path(wfilename, filename) < 0)
588+
} else if (xutftowcs_path(wfilename, filename) < 0)
591589
return NULL;
592590

593591
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -2412,14 +2410,16 @@ static void setup_windows_environment(void)
24122410
}
24132411
}
24142412

2415-
int is_valid_win32_path(const char *path)
2413+
int is_valid_win32_path(const char *path, int allow_literal_nul)
24162414
{
2415+
const char *p = path;
24172416
int preceding_space_or_period = 0, i = 0, periods = 0;
24182417

24192418
if (!protect_ntfs)
24202419
return 1;
24212420

24222421
skip_dos_drive_prefix((char **)&path);
2422+
goto segment_start;
24232423

24242424
for (;;) {
24252425
char c = *(path++);
@@ -2434,7 +2434,83 @@ int is_valid_win32_path(const char *path)
24342434
return 1;
24352435

24362436
i = periods = preceding_space_or_period = 0;
2437-
continue;
2437+
2438+
segment_start:
2439+
switch (*path) {
2440+
case 'a': case 'A': /* AUX */
2441+
if (((c = path[++i]) != 'u' && c != 'U') ||
2442+
((c = path[++i]) != 'x' && c != 'X')) {
2443+
not_a_reserved_name:
2444+
path += i;
2445+
continue;
2446+
}
2447+
break;
2448+
case 'c': case 'C': /* COM<N>, CON, CONIN$, CONOUT$ */
2449+
if ((c = path[++i]) != 'o' && c != 'O')
2450+
goto not_a_reserved_name;
2451+
c = path[++i];
2452+
if (c == 'm' || c == 'M') { /* COM<N> */
2453+
if (!isdigit(path[++i]))
2454+
goto not_a_reserved_name;
2455+
} else if (c == 'n' || c == 'N') { /* CON */
2456+
c = path[i + 1];
2457+
if ((c == 'i' || c == 'I') &&
2458+
((c = path[i + 2]) == 'n' ||
2459+
c == 'N') &&
2460+
path[i + 3] == '$')
2461+
i += 3; /* CONIN$ */
2462+
else if ((c == 'o' || c == 'O') &&
2463+
((c = path[i + 2]) == 'u' ||
2464+
c == 'U') &&
2465+
((c = path[i + 3]) == 't' ||
2466+
c == 'T') &&
2467+
path[i + 4] == '$')
2468+
i += 4; /* CONOUT$ */
2469+
} else
2470+
goto not_a_reserved_name;
2471+
break;
2472+
case 'l': case 'L': /* LPT<N> */
2473+
if (((c = path[++i]) != 'p' && c != 'P') ||
2474+
((c = path[++i]) != 't' && c != 'T') ||
2475+
!isdigit(path[++i]))
2476+
goto not_a_reserved_name;
2477+
break;
2478+
case 'n': case 'N': /* NUL */
2479+
if (((c = path[++i]) != 'u' && c != 'U') ||
2480+
((c = path[++i]) != 'l' && c != 'L') ||
2481+
(allow_literal_nul &&
2482+
!path[i + 1] && p == path))
2483+
goto not_a_reserved_name;
2484+
break;
2485+
case 'p': case 'P': /* PRN */
2486+
if (((c = path[++i]) != 'r' && c != 'R') ||
2487+
((c = path[++i]) != 'n' && c != 'N'))
2488+
goto not_a_reserved_name;
2489+
break;
2490+
default:
2491+
continue;
2492+
}
2493+
2494+
/*
2495+
* So far, this looks like a reserved name. Let's see
2496+
* whether it actually is one: trailing spaces, a file
2497+
* extension, or an NTFS Alternate Data Stream do not
2498+
* matter, the name is still reserved if any of those
2499+
* follow immediately after the actual name.
2500+
*/
2501+
i++;
2502+
if (path[i] == ' ') {
2503+
preceding_space_or_period = 1;
2504+
while (path[++i] == ' ')
2505+
; /* skip all spaces */
2506+
}
2507+
2508+
c = path[i];
2509+
if (c && c != '.' && c != ':' && c != '/' && c != '\\')
2510+
goto not_a_reserved_name;
2511+
2512+
/* contains reserved name */
2513+
return 0;
24382514
case '.':
24392515
periods++;
24402516
/* fallthru */

compat/mingw.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,10 +461,17 @@ char *mingw_query_user_email(void);
461461
*
462462
* - contain any of the reserved characters, e.g. `:`, `;`, `*`, etc
463463
*
464+
* - correspond to reserved names (such as `AUX`, `PRN`, etc)
465+
*
466+
* The `allow_literal_nul` parameter controls whether the path `NUL` should
467+
* be considered valid (this makes sense e.g. before opening files, as it is
468+
* perfectly legitimate to open `NUL` on Windows, just as it is to open
469+
* `/dev/null` on Unix/Linux).
470+
*
464471
* Returns 1 upon success, otherwise 0.
465472
*/
466-
int is_valid_win32_path(const char *path);
467-
#define is_valid_path(path) is_valid_win32_path(path)
473+
int is_valid_win32_path(const char *path, int allow_literal_nul);
474+
#define is_valid_path(path) is_valid_win32_path(path, 0)
468475

469476
/**
470477
* Converts UTF-8 encoded string to UTF-16LE.

t/t0060-path-utils.sh

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,19 +465,28 @@ test_expect_success 'match .gitmodules' '
465465
'
466466

467467
test_expect_success MINGW 'is_valid_path() on Windows' '
468-
test-tool path-utils is_valid_path \
468+
test-tool path-utils is_valid_path \
469469
win32 \
470470
"win32 x" \
471471
../hello.txt \
472472
C:\\git \
473+
comm \
474+
conout.c \
475+
lptN \
473476
\
474477
--not \
475478
"win32 " \
476479
"win32 /x " \
477480
"win32." \
478481
"win32 . ." \
479482
.../hello.txt \
480-
colon:test
483+
colon:test \
484+
"AUX.c" \
485+
"abc/conOut\$ .xyz/test" \
486+
lpt8 \
487+
"lpt*" \
488+
Nul \
489+
"PRN./abc"
481490
'
482491

483492
test_done

0 commit comments

Comments
 (0)