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 <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Johannes Schindelin committed
Dec 21, 2019 at 22:05 UTC
4dc42c6c1867a52e22f1f04a1a247b5a7538b8af
3 files changed
+110
-18
compat/mingw.c
+90
-14
@@ -393,7 +393,7 @@ int mingw_mkdir(const char *path, int mode)
393
int ret;
394
wchar_t wpath[MAX_PATH];
395
396
- if (!is_valid_win32_path(path)) {
396
+ if (!is_valid_win32_path(path, 0)) {
397
errno = EINVAL;
398
return -1;
399
}
@@ -479,7 +479,7 @@ int mingw_open (const char *filename, int oflags, ...)
479
mode = va_arg(args, int);
480
va_end(args);
481
482
- if (!is_valid_win32_path(filename)) {
482
+ if (!is_valid_win32_path(filename, !create)) {
483
errno = create ? EINVAL : ENOENT;
484
return -1;
485
}
@@ -550,14 +550,13 @@ FILE *mingw_fopen (const char *filename, const char *otype)
550
int hide = needs_hiding(filename);
551
FILE *file;
552
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)) {
556
int create = otype && strchr(otype, 'w');
557
errno = create ? EINVAL : ENOENT;
558
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)
560
return NULL;
561
562
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -580,14 +579,13 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
579
int hide = needs_hiding(filename);
580
FILE *file;
581
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)) {
585
int create = otype && strchr(otype, 'w');
586
errno = create ? EINVAL : ENOENT;
587
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)
589
return NULL;
590
591
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -2412,14 +2410,16 @@ static void setup_windows_environment(void)
2410
}
2411
}
2412
2415
-int is_valid_win32_path(const char *path)
2413
+int is_valid_win32_path(const char *path, int allow_literal_nul)
2414
{
2415
+ const char *p = path;
2416
int preceding_space_or_period = 0, i = 0, periods = 0;
2417
2418
if (!protect_ntfs)
2419
return 1;
2420
2421
skip_dos_drive_prefix((char **)&path);
2422
+ goto segment_start;
2423
2424
for (;;) {
2425
char c = *(path++);
@@ -2434,7 +2434,83 @@ int is_valid_win32_path(const char *path)
2434
return 1;
2435
2436
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;
2514
case '.':
2515
periods++;
2516
/* fallthru */
compat/mingw.h
+9
-2
@@ -461,10 +461,17 @@ char *mingw_query_user_email(void);
461
*
462
* - contain any of the reserved characters, e.g. `:`, `;`, `*`, etc
463
*
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
+ *
471
* Returns 1 upon success, otherwise 0.
472
*/
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)
475
476
/**
477
* Converts UTF-8 encoded string to UTF-16LE.
t/t0060-path-utils.sh
+11
-2
@@ -465,11 +465,14 @@ test_expect_success 'match .gitmodules' '
465
'
466
467
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 \
469
win32 \
470
"win32 x" \
471
../hello.txt \
472
C:\\git \
473
+ comm \
474
+ conout.c \
475
+ lptN \
476
\
477
--not \
478
"win32 " \
@@ -477,7 +480,13 @@ test_expect_success MINGW 'is_valid_path() on Windows' '
480
"win32." \
481
"win32 . ." \
482
.../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"
490
'
491
492
test_done