@matheusbsbs / matheus-bagliano / commits / 4dc42c6c18
Fork 0

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