]> git.ipfire.org Git - thirdparty/git.git/commitdiff
mingw: refuse paths containing reserved names
authorJohannes Schindelin <johannes.schindelin@gmx.de>
Sat, 21 Dec 2019 22:05:01 +0000 (22:05 +0000)
committerJunio C Hamano <gitster@pobox.com>
Sun, 22 Dec 2019 00:09:07 +0000 (16:09 -0800)
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>
compat/mingw.c
compat/mingw.h
t/t0060-path-utils.sh

index 03c4538ec886aebb8e5c25c370f25aa64e0c637b..f482ecd6daa759df4476d231447f7dc22014599e 100644 (file)
@@ -393,7 +393,7 @@ int mingw_mkdir(const char *path, int mode)
        int ret;
        wchar_t wpath[MAX_PATH];
 
-       if (!is_valid_win32_path(path)) {
+       if (!is_valid_win32_path(path, 0)) {
                errno = EINVAL;
                return -1;
        }
@@ -479,7 +479,7 @@ int mingw_open (const char *filename, int oflags, ...)
        mode = va_arg(args, int);
        va_end(args);
 
-       if (!is_valid_win32_path(filename)) {
+       if (!is_valid_win32_path(filename, !create)) {
                errno = create ? EINVAL : ENOENT;
                return -1;
        }
@@ -550,14 +550,13 @@ FILE *mingw_fopen (const char *filename, const char *otype)
        int hide = needs_hiding(filename);
        FILE *file;
        wchar_t wfilename[MAX_PATH], wotype[4];
-       if (!is_valid_win32_path(filename)) {
+       if (filename && !strcmp(filename, "/dev/null"))
+               wcscpy(wfilename, L"nul");
+       else if (!is_valid_win32_path(filename, 1)) {
                int create = otype && strchr(otype, 'w');
                errno = create ? EINVAL : ENOENT;
                return NULL;
-       }
-       if (filename && !strcmp(filename, "/dev/null"))
-               wcscpy(wfilename, L"nul");
-       else if (xutftowcs_path(wfilename, filename) < 0)
+       } else if (xutftowcs_path(wfilename, filename) < 0)
                return NULL;
 
        if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -580,14 +579,13 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
        int hide = needs_hiding(filename);
        FILE *file;
        wchar_t wfilename[MAX_PATH], wotype[4];
-       if (!is_valid_win32_path(filename)) {
+       if (filename && !strcmp(filename, "/dev/null"))
+               wcscpy(wfilename, L"nul");
+       else if (!is_valid_win32_path(filename, 1)) {
                int create = otype && strchr(otype, 'w');
                errno = create ? EINVAL : ENOENT;
                return NULL;
-       }
-       if (filename && !strcmp(filename, "/dev/null"))
-               wcscpy(wfilename, L"nul");
-       else if (xutftowcs_path(wfilename, filename) < 0)
+       } else if (xutftowcs_path(wfilename, filename) < 0)
                return NULL;
 
        if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -2412,14 +2410,16 @@ static void setup_windows_environment(void)
        }
 }
 
-int is_valid_win32_path(const char *path)
+int is_valid_win32_path(const char *path, int allow_literal_nul)
 {
+       const char *p = path;
        int preceding_space_or_period = 0, i = 0, periods = 0;
 
        if (!protect_ntfs)
                return 1;
 
        skip_dos_drive_prefix((char **)&path);
+       goto segment_start;
 
        for (;;) {
                char c = *(path++);
@@ -2434,7 +2434,83 @@ int is_valid_win32_path(const char *path)
                                return 1;
 
                        i = periods = preceding_space_or_period = 0;
-                       continue;
+
+segment_start:
+                       switch (*path) {
+                       case 'a': case 'A': /* AUX */
+                               if (((c = path[++i]) != 'u' && c != 'U') ||
+                                   ((c = path[++i]) != 'x' && c != 'X')) {
+not_a_reserved_name:
+                                       path += i;
+                                       continue;
+                               }
+                               break;
+                       case 'c': case 'C': /* COM<N>, CON, CONIN$, CONOUT$ */
+                               if ((c = path[++i]) != 'o' && c != 'O')
+                                       goto not_a_reserved_name;
+                               c = path[++i];
+                               if (c == 'm' || c == 'M') { /* COM<N> */
+                                       if (!isdigit(path[++i]))
+                                               goto not_a_reserved_name;
+                               } else if (c == 'n' || c == 'N') { /* CON */
+                                       c = path[i + 1];
+                                       if ((c == 'i' || c == 'I') &&
+                                           ((c = path[i + 2]) == 'n' ||
+                                            c == 'N') &&
+                                           path[i + 3] == '$')
+                                               i += 3; /* CONIN$ */
+                                       else if ((c == 'o' || c == 'O') &&
+                                                ((c = path[i + 2]) == 'u' ||
+                                                 c == 'U') &&
+                                                ((c = path[i + 3]) == 't' ||
+                                                 c == 'T') &&
+                                                path[i + 4] == '$')
+                                               i += 4; /* CONOUT$ */
+                               } else
+                                       goto not_a_reserved_name;
+                               break;
+                       case 'l': case 'L': /* LPT<N> */
+                               if (((c = path[++i]) != 'p' && c != 'P') ||
+                                   ((c = path[++i]) != 't' && c != 'T') ||
+                                   !isdigit(path[++i]))
+                                       goto not_a_reserved_name;
+                               break;
+                       case 'n': case 'N': /* NUL */
+                               if (((c = path[++i]) != 'u' && c != 'U') ||
+                                   ((c = path[++i]) != 'l' && c != 'L') ||
+                                   (allow_literal_nul &&
+                                    !path[i + 1] && p == path))
+                                       goto not_a_reserved_name;
+                               break;
+                       case 'p': case 'P': /* PRN */
+                               if (((c = path[++i]) != 'r' && c != 'R') ||
+                                   ((c = path[++i]) != 'n' && c != 'N'))
+                                       goto not_a_reserved_name;
+                               break;
+                       default:
+                               continue;
+                       }
+
+                       /*
+                        * So far, this looks like a reserved name. Let's see
+                        * whether it actually is one: trailing spaces, a file
+                        * extension, or an NTFS Alternate Data Stream do not
+                        * matter, the name is still reserved if any of those
+                        * follow immediately after the actual name.
+                        */
+                       i++;
+                       if (path[i] == ' ') {
+                               preceding_space_or_period = 1;
+                               while (path[++i] == ' ')
+                                       ; /* skip all spaces */
+                       }
+
+                       c = path[i];
+                       if (c && c != '.' && c != ':' && c != '/' && c != '\\')
+                               goto not_a_reserved_name;
+
+                       /* contains reserved name */
+                       return 0;
                case '.':
                        periods++;
                        /* fallthru */
index 04ca731a6b76887390fae4f2fa7b5f7c8c56905a..ebc1e6a773bf5cf8958b9301b7ae7a6c70135a02 100644 (file)
@@ -461,10 +461,17 @@ char *mingw_query_user_email(void);
  *
  * - contain any of the reserved characters, e.g. `:`, `;`, `*`, etc
  *
+ * - correspond to reserved names (such as `AUX`, `PRN`, etc)
+ *
+ * The `allow_literal_nul` parameter controls whether the path `NUL` should
+ * be considered valid (this makes sense e.g. before opening files, as it is
+ * perfectly legitimate to open `NUL` on Windows, just as it is to open
+ * `/dev/null` on Unix/Linux).
+ *
  * Returns 1 upon success, otherwise 0.
  */
-int is_valid_win32_path(const char *path);
-#define is_valid_path(path) is_valid_win32_path(path)
+int is_valid_win32_path(const char *path, int allow_literal_nul);
+#define is_valid_path(path) is_valid_win32_path(path, 0)
 
 /**
  * Converts UTF-8 encoded string to UTF-16LE.
index b193ed42050509bc3951e2faae22fbecf0d0b044..eaf3be94e37a17cea8a5fe3484b38be1441aa50e 100755 (executable)
@@ -465,11 +465,14 @@ test_expect_success 'match .gitmodules' '
 '
 
 test_expect_success MINGW 'is_valid_path() on Windows' '
-       test-tool path-utils is_valid_path \
+       test-tool path-utils is_valid_path \
                win32 \
                "win32 x" \
                ../hello.txt \
                C:\\git \
+               comm \
+               conout.c \
+               lptN \
                \
                --not \
                "win32 "  \
@@ -477,7 +480,13 @@ test_expect_success MINGW 'is_valid_path() on Windows' '
                "win32."  \
                "win32 . ." \
                .../hello.txt \
-               colon:test
+               colon:test \
+               "AUX.c" \
+               "abc/conOut\$  .xyz/test" \
+               lpt8 \
+               "lpt*" \
+               Nul \
+               "PRN./abc"
 '
 
 test_done