From d316eefeb4b857238f2e9f15322239b00e757277 Mon Sep 17 00:00:00 2001
From: Steve Dower <steve.dower@python.org>
Date: Mon, 5 Mar 2018 09:03:42 -0800
Subject: [PATCH 1/2] bpo-33001: Minimal fix to prevent buffer overrun in
 os.symlink

---
 Lib/test/test_os.py                                | 42 ++++++++++
 .../2018-03-05-10-09-51.bpo-33001.elj4Aa.rst       |  1 +
 Modules/posixmodule.c                              | 94 +++++++++++++---------
 3 files changed, 99 insertions(+), 38 deletions(-)
 create mode 100644 Misc/NEWS.d/next/Security/2018-03-05-10-09-51.bpo-33001.elj4Aa.rst

diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
index bb5d2e3429ce..e000b80f5e1b 100644
--- a/Lib/test/test_os.py
+++ b/Lib/test/test_os.py
@@ -2117,6 +2117,48 @@ def test_12084(self):
             os.remove(file1)
             shutil.rmtree(level1)
 
+    def test_buffer_overflow(self):
+        # Older versions would have a buffer overflow when detecting
+        # whether a link source was a directory. This test ensures we
+        # no longer crash, but does not otherwise validate the behavior
+        segment = 'X' * 27
+        path = os.path.join(*[segment] * 10)
+        test_cases = [
+            # overflow with absolute src
+            ('\\' + path, segment),
+            # overflow dest with relative src
+            (segment, path),
+            # overflow dest when appending '\\' for join
+            (segment, path[:261]),
+            # overflow when joining src
+            (path[:180], path[:180]),
+        ]
+        for src, dest in test_cases:
+            try:
+                os.symlink(src, dest)
+            except FileNotFoundError:
+                pass
+            else:
+                try:
+                    os.remove(dest)
+                except OSError:
+                    pass
+            # Also test with bytes, since that is a separate code path.
+            # However, because of the stack layout, it is not possible
+            # to exploit the overflow on Python 3.5 using bytes
+            try:
+                os.symlink(os.fsencode(src), os.fsencode(dest))
+            except ValueError:
+                # Conversion function checks for len(arg) >= 260
+                pass
+            except FileNotFoundError:
+                pass
+            else:
+                try:
+                    os.remove(dest)
+                except OSError:
+                    pass
+
 
 @unittest.skipUnless(sys.platform == "win32", "Win32 specific tests")
 class Win32JunctionTests(unittest.TestCase):
diff --git a/Misc/NEWS.d/next/Security/2018-03-05-10-09-51.bpo-33001.elj4Aa.rst b/Misc/NEWS.d/next/Security/2018-03-05-10-09-51.bpo-33001.elj4Aa.rst
new file mode 100644
index 000000000000..2acbac9e1af6
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2018-03-05-10-09-51.bpo-33001.elj4Aa.rst
@@ -0,0 +1 @@
+Minimal fix to prevent buffer overrun in os.symlink on Windows
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index d42416b0270e..102c5405bd59 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -7259,39 +7259,50 @@ check_CreateSymbolicLink(void)
     return (Py_CreateSymbolicLinkW && Py_CreateSymbolicLinkA);
 }
 
-/* Remove the last portion of the path */
-static void
+/* Remove the last portion of the path - return 0 on success */
+static int
 _dirnameW(WCHAR *path)
 {
     WCHAR *ptr;
+    size_t length = wcsnlen_s(path, MAX_PATH);
+    if (length == MAX_PATH) {
+        return -1;
+    }
 
     /* walk the path from the end until a backslash is encountered */
-    for(ptr = path + wcslen(path); ptr != path; ptr--) {
+    for(ptr = path + length; ptr != path; ptr--) {
         if (*ptr == L'\\' || *ptr == L'/')
             break;
     }
     *ptr = 0;
+    return 0;
 }
 
-/* Remove the last portion of the path */
-static void
+/* Remove the last portion of the path - return 0 on success */
+static int
 _dirnameA(char *path)
 {
     char *ptr;
+    size_t length = strnlen_s(path, MAX_PATH);
+    if (length == MAX_PATH) {
+        return -1;
+    }
 
     /* walk the path from the end until a backslash is encountered */
-    for(ptr = path + strlen(path); ptr != path; ptr--) {
+    for(ptr = path + length; ptr != path; ptr--) {
         if (*ptr == '\\' || *ptr == '/')
             break;
     }
     *ptr = 0;
+    return 0;
 }
 
 /* Is this path absolute? */
 static int
 _is_absW(const WCHAR *path)
 {
-    return path[0] == L'\\' || path[0] == L'/' || path[1] == L':';
+    return path[0] == L'\\' || path[0] == L'/' ||
+        (path[0] && path[1] == L':');
 
 }
 
@@ -7299,50 +7310,47 @@ _is_absW(const WCHAR *path)
 static int
 _is_absA(const char *path)
 {
-    return path[0] == '\\' || path[0] == '/' || path[1] == ':';
+    return path[0] == '\\' || path[0] == '/' ||
+        (path[0] && path[1] == ':');
 
 }
 
-/* join root and rest with a backslash */
-static void
+/* join root and rest with a backslash - return 0 on success */
+static int
 _joinW(WCHAR *dest_path, const WCHAR *root, const WCHAR *rest)
 {
-    size_t root_len;
-
     if (_is_absW(rest)) {
-        wcscpy(dest_path, rest);
-        return;
+        return wcscpy_s(dest_path, MAX_PATH, rest);
     }
 
-    root_len = wcslen(root);
+    if (wcscpy_s(dest_path, MAX_PATH, root)) {
+        return -1;
+    }
 
-    wcscpy(dest_path, root);
-    if(root_len) {
-        dest_path[root_len] = L'\\';
-        root_len++;
+    if (dest_path[0] && wcscat_s(dest_path, MAX_PATH, L"\\")) {
+        return -1;
     }
-    wcscpy(dest_path+root_len, rest);
+
+    return wcscat_s(dest_path, MAX_PATH, rest);
 }
 
-/* join root and rest with a backslash */
-static void
+/* join root and rest with a backslash - return 0 on success */
+static int
 _joinA(char *dest_path, const char *root, const char *rest)
 {
-    size_t root_len;
-
     if (_is_absA(rest)) {
-        strcpy(dest_path, rest);
-        return;
+        return strcpy_s(dest_path, MAX_PATH, rest);
     }
 
-    root_len = strlen(root);
+    if (strcpy_s(dest_path, MAX_PATH, root)) {
+        return -1;
+    }
 
-    strcpy(dest_path, root);
-    if(root_len) {
-        dest_path[root_len] = '\\';
-        root_len++;
+    if (dest_path[0] && strcat_s(dest_path, MAX_PATH, "\\")) {
+        return -1;
     }
-    strcpy(dest_path+root_len, rest);
+
+    return strcat_s(dest_path, MAX_PATH, rest);
 }
 
 /* Return True if the path at src relative to dest is a directory */
@@ -7354,10 +7362,14 @@ _check_dirW(WCHAR *src, WCHAR *dest)
     WCHAR src_resolved[MAX_PATH] = L"";
 
     /* dest_parent = os.path.dirname(dest) */
-    wcscpy(dest_parent, dest);
-    _dirnameW(dest_parent);
+    if (wcscpy_s(dest_parent, MAX_PATH, dest) ||
+        _dirnameW(dest_parent)) {
+        return 0;
+    }
     /* src_resolved = os.path.join(dest_parent, src) */
-    _joinW(src_resolved, dest_parent, src);
+    if (_joinW(src_resolved, dest_parent, src)) {
+        return 0;
+    }
     return (
         GetFileAttributesExW(src_resolved, GetFileExInfoStandard, &src_info)
         && src_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY
@@ -7373,10 +7385,14 @@ _check_dirA(char *src, char *dest)
     char src_resolved[MAX_PATH] = "";
 
     /* dest_parent = os.path.dirname(dest) */
-    strcpy(dest_parent, dest);
-    _dirnameA(dest_parent);
+    if (strcpy_s(dest_parent, MAX_PATH, dest) ||
+        _dirnameA(dest_parent)) {
+        return 0;
+    }
     /* src_resolved = os.path.join(dest_parent, src) */
-    _joinA(src_resolved, dest_parent, src);
+    if (_joinA(src_resolved, dest_parent, src)) {
+        return 0;
+    }
     return (
         GetFileAttributesExA(src_resolved, GetFileExInfoStandard, &src_info)
         && src_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY
@@ -7441,6 +7457,7 @@ os_symlink_impl(PyObject *module, path_t *src, path_t *dst,
 #ifdef MS_WINDOWS
 
     Py_BEGIN_ALLOW_THREADS
+    _Py_BEGIN_SUPPRESS_IPH
     if (dst->wide) {
         /* if src is a directory, ensure target_is_directory==1 */
         target_is_directory |= _check_dirW(src->wide, dst->wide);
@@ -7453,6 +7470,7 @@ os_symlink_impl(PyObject *module, path_t *src, path_t *dst,
         result = Py_CreateSymbolicLinkA(dst->narrow, src->narrow,
                                         target_is_directory);
     }
+    _Py_END_SUPPRESS_IPH
     Py_END_ALLOW_THREADS
 
     if (!result)

From ac732bec4d227a38ebb57a83a6caef0df671a219 Mon Sep 17 00:00:00 2001
From: Steve Dower <steve.dower@python.org>
Date: Mon, 5 Mar 2018 13:23:22 -0800
Subject: [PATCH 2/2] Remove invalid test

---
 Lib/test/test_os.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
index e000b80f5e1b..e2077383bf47 100644
--- a/Lib/test/test_os.py
+++ b/Lib/test/test_os.py
@@ -2128,8 +2128,6 @@ def test_buffer_overflow(self):
             ('\\' + path, segment),
             # overflow dest with relative src
             (segment, path),
-            # overflow dest when appending '\\' for join
-            (segment, path[:261]),
             # overflow when joining src
             (path[:180], path[:180]),
         ]
