Skip to content

Commit ed76aa0

Browse files
committed
Merge pull request git-for-windows#2440 from dscho/mingw-reserved-filenames-gfw
Refuse to write to reserved filenames
2 parents 6afb533 + 84ff736 commit ed76aa0

File tree

3 files changed

+122
-24
lines changed

3 files changed

+122
-24
lines changed

compat/mingw.c

Lines changed: 102 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ int mingw_mkdir(const char *path, int mode)
417417
int ret;
418418
wchar_t wpath[MAX_PATH];
419419

420-
if (!is_valid_win32_path(path)) {
420+
if (!is_valid_win32_path(path, 0)) {
421421
errno = EINVAL;
422422
return -1;
423423
}
@@ -514,21 +514,21 @@ int mingw_open (const char *filename, int oflags, ...)
514514
mode = va_arg(args, int);
515515
va_end(args);
516516

517-
if (!is_valid_win32_path(filename)) {
517+
if (!is_valid_win32_path(filename, !create)) {
518518
errno = create ? EINVAL : ENOENT;
519519
return -1;
520520
}
521521

522-
if (filename && !strcmp(filename, "/dev/null"))
523-
filename = "nul";
524-
525522
if ((oflags & O_APPEND) && !is_local_named_pipe_path(filename))
526523
open_fn = mingw_open_append;
527524
else
528525
open_fn = _wopen;
529526

530-
if (xutftowcs_path(wfilename, filename) < 0)
527+
if (filename && !strcmp(filename, "/dev/null"))
528+
wcscpy(wfilename, L"nul");
529+
else if (xutftowcs_path(wfilename, filename) < 0)
531530
return -1;
531+
532532
fd = open_fn(wfilename, oflags, mode);
533533

534534
if (fd < 0 && (oflags & O_ACCMODE) != O_RDONLY && errno == EACCES) {
@@ -585,16 +585,18 @@ FILE *mingw_fopen (const char *filename, const char *otype)
585585
int hide = needs_hiding(filename);
586586
FILE *file;
587587
wchar_t wfilename[MAX_PATH], wotype[4];
588-
if (!is_valid_win32_path(filename)) {
588+
if (filename && !strcmp(filename, "/dev/null"))
589+
wcscpy(wfilename, L"nul");
590+
else if (!is_valid_win32_path(filename, 1)) {
589591
int create = otype && strchr(otype, 'w');
590592
errno = create ? EINVAL : ENOENT;
591593
return NULL;
592-
}
593-
if (filename && !strcmp(filename, "/dev/null"))
594-
filename = "nul";
595-
if (xutftowcs_path(wfilename, filename) < 0 ||
596-
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
594+
} else if (xutftowcs_path(wfilename, filename) < 0)
597595
return NULL;
596+
597+
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
598+
return NULL;
599+
598600
if (hide && !access(filename, F_OK) && set_hidden_flag(wfilename, 0)) {
599601
error("could not unhide %s", filename);
600602
return NULL;
@@ -612,16 +614,18 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
612614
int hide = needs_hiding(filename);
613615
FILE *file;
614616
wchar_t wfilename[MAX_PATH], wotype[4];
615-
if (!is_valid_win32_path(filename)) {
617+
if (filename && !strcmp(filename, "/dev/null"))
618+
wcscpy(wfilename, L"nul");
619+
else if (!is_valid_win32_path(filename, 1)) {
616620
int create = otype && strchr(otype, 'w');
617621
errno = create ? EINVAL : ENOENT;
618622
return NULL;
619-
}
620-
if (filename && !strcmp(filename, "/dev/null"))
621-
filename = "nul";
622-
if (xutftowcs_path(wfilename, filename) < 0 ||
623-
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
623+
} else if (xutftowcs_path(wfilename, filename) < 0)
624+
return NULL;
625+
626+
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
624627
return NULL;
628+
625629
if (hide && !access(filename, F_OK) && set_hidden_flag(wfilename, 0)) {
626630
error("could not unhide %s", filename);
627631
return NULL;
@@ -2746,14 +2750,16 @@ static void setup_windows_environment(void)
27462750
}
27472751
}
27482752

2749-
int is_valid_win32_path(const char *path)
2753+
int is_valid_win32_path(const char *path, int allow_literal_nul)
27502754
{
2755+
const char *p = path;
27512756
int preceding_space_or_period = 0, i = 0, periods = 0;
27522757

27532758
if (!protect_ntfs)
27542759
return 1;
27552760

27562761
skip_dos_drive_prefix((char **)&path);
2762+
goto segment_start;
27572763

27582764
for (;;) {
27592765
char c = *(path++);
@@ -2768,7 +2774,83 @@ int is_valid_win32_path(const char *path)
27682774
return 1;
27692775

27702776
i = periods = preceding_space_or_period = 0;
2771-
continue;
2777+
2778+
segment_start:
2779+
switch (*path) {
2780+
case 'a': case 'A': /* AUX */
2781+
if (((c = path[++i]) != 'u' && c != 'U') ||
2782+
((c = path[++i]) != 'x' && c != 'X')) {
2783+
not_a_reserved_name:
2784+
path += i;
2785+
continue;
2786+
}
2787+
break;
2788+
case 'c': case 'C': /* COM<N>, CON, CONIN$, CONOUT$ */
2789+
if ((c = path[++i]) != 'o' && c != 'O')
2790+
goto not_a_reserved_name;
2791+
c = path[++i];
2792+
if (c == 'm' || c == 'M') { /* COM<N> */
2793+
if (!isdigit(path[++i]))
2794+
goto not_a_reserved_name;
2795+
} else if (c == 'n' || c == 'N') { /* CON */
2796+
c = path[i + 1];
2797+
if ((c == 'i' || c == 'I') &&
2798+
((c = path[i + 2]) == 'n' ||
2799+
c == 'N') &&
2800+
path[i + 3] == '$')
2801+
i += 3; /* CONIN$ */
2802+
else if ((c == 'o' || c == 'O') &&
2803+
((c = path[i + 2]) == 'u' ||
2804+
c == 'U') &&
2805+
((c = path[i + 3]) == 't' ||
2806+
c == 'T') &&
2807+
path[i + 4] == '$')
2808+
i += 4; /* CONOUT$ */
2809+
} else
2810+
goto not_a_reserved_name;
2811+
break;
2812+
case 'l': case 'L': /* LPT<N> */
2813+
if (((c = path[++i]) != 'p' && c != 'P') ||
2814+
((c = path[++i]) != 't' && c != 'T') ||
2815+
!isdigit(path[++i]))
2816+
goto not_a_reserved_name;
2817+
break;
2818+
case 'n': case 'N': /* NUL */
2819+
if (((c = path[++i]) != 'u' && c != 'U') ||
2820+
((c = path[++i]) != 'l' && c != 'L') ||
2821+
(allow_literal_nul &&
2822+
!path[i + 1] && p == path))
2823+
goto not_a_reserved_name;
2824+
break;
2825+
case 'p': case 'P': /* PRN */
2826+
if (((c = path[++i]) != 'r' && c != 'R') ||
2827+
((c = path[++i]) != 'n' && c != 'N'))
2828+
goto not_a_reserved_name;
2829+
break;
2830+
default:
2831+
continue;
2832+
}
2833+
2834+
/*
2835+
* So far, this looks like a reserved name. Let's see
2836+
* whether it actually is one: trailing spaces, a file
2837+
* extension, or an NTFS Alternate Data Stream do not
2838+
* matter, the name is still reserved if any of those
2839+
* follow immediately after the actual name.
2840+
*/
2841+
i++;
2842+
if (path[i] == ' ') {
2843+
preceding_space_or_period = 1;
2844+
while (path[++i] == ' ')
2845+
; /* skip all spaces */
2846+
}
2847+
2848+
c = path[i];
2849+
if (c && c != '.' && c != ':' && c != '/' && c != '\\')
2850+
goto not_a_reserved_name;
2851+
2852+
/* contains reserved name */
2853+
return 0;
27722854
case '.':
27732855
periods++;
27742856
/* fallthru */

compat/mingw.h

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

473480
/**
474481
* 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
@@ -469,19 +469,28 @@ test_expect_success 'match .gitmodules' '
469469
'
470470

471471
test_expect_success MINGW 'is_valid_path() on Windows' '
472-
test-tool path-utils is_valid_path \
472+
test-tool path-utils is_valid_path \
473473
win32 \
474474
"win32 x" \
475475
../hello.txt \
476476
C:\\git \
477+
comm \
478+
conout.c \
479+
lptN \
477480
\
478481
--not \
479482
"win32 " \
480483
"win32 /x " \
481484
"win32." \
482485
"win32 . ." \
483486
.../hello.txt \
484-
colon:test
487+
colon:test \
488+
"AUX.c" \
489+
"abc/conOut\$ .xyz/test" \
490+
lpt8 \
491+
"lpt*" \
492+
Nul \
493+
"PRN./abc"
485494
'
486495

487496
test_done

0 commit comments

Comments
 (0)