Skip to content

ntpath.realpath() mishandles filenames that resemble drives #102475

Description

@barneygale

The realpath() docs say:

If strict is False, the path is resolved as far as possible and any remainder is appended without checking whether it exists.

Note the word "appended". In fact, realpath() uses os.path.join() to join the path segments, and as we all know/love, os.path.join() supports resetting the drive or root, thus discarding prior parts. As a result:

>>> os.path.realpath('c:/a:b')
'a:b'  # should be 'c:/a:b'

Linked PRs

Activity

  1. kostyafarber commented on Mar 18, 2023

    @kostyafarber
    Contributor

    Any ideas on how to go about fixing this one? Keen to work on it but it seems (with regards to the linked issue) there are other things related to those to sort out first?

  2. barneygale commented on Mar 19, 2023

    @barneygale
    ContributorAuthor

    Thanks for taking a look!

    Essentially: ntpath.realpath() should not call ntpath.join() internally. Instead, it should use a more naive form of joining, like sep.join() or simply head + sep + tail. Care must be taken not to double up slashes. A small utility function may be warranted.

    I suspect the main problematic case is here:

    tail = join(name, tail) if tail else name

    ... but there are other usages of join() which will need some thought.

  3. kostyafarber commented on Mar 19, 2023

    @kostyafarber
    Contributor

    Thanks for the detailed explanation. I'll have a go at it.

  4. eryksun commented on Mar 19, 2023

    @eryksun
    Contributor

    Any ideas on how to go about fixing this one?

    Define a custom join() function inside of ntpath._getfinalpathname_nonstrict(). It can be simplified since it only joins two paths, the second of which is a simple relative path. For example:

            if isinstance(path, bytes):        
                tail = b''
                sep = b'\\'
            else:
                tail = ''
                sep = '\\'
    
            def join(path, tail):
                if path[-1:] == sep or not tail:
                    return path + tail
                return path + sep + tail

    Use this function for all of the four join() calls in _getfinalpathname_nonstrict().

  5. eryksun commented on Mar 19, 2023

    @eryksun
    Contributor

    Use this function for all of the four join() calls in _getfinalpathname_nonstrict().

    I said four join() calls instead of 3 because I was counting the following as well:

    cpython/Lib/ntpath.py

    Lines 695 to 696 in 4d1f033

    if path and not name:
    return path + tail

    The working directory on a non-existing drive should be the root path. For example, the following result is wrong:

    >>> ntpath.exists('Z:')
    False
    >>> ntpath.realpath('Z:spam')
    'Z:spam'
  6. barneygale commented on Mar 19, 2023

    @barneygale
    ContributorAuthor

    Eryk, is that function necessary in all four cases? I wonder if we can guarantee that path does or does not end with a separator in any of them.

  7. kostyafarber commented on Mar 19, 2023

    @kostyafarber
    Contributor

    Any ideas on how to go about fixing this one?

    Define a custom join() function inside of ntpath._getfinalpathname_nonstrict(). It can be simplified since it only joins two paths, the second of which is a simple relative path. For example:

            if isinstance(path, bytes):        
    
                tail = b''
    
                sep = b'\\'
    
            else:
    
                tail = ''
    
                sep = '\\'
    
    
    
            def join(path, tail):
    
                if path[-1:] == sep or not tail:
    
                    return path + tail
    
                return path + sep + tail

    Use this function for all of the four join() calls in _getfinalpathname_nonstrict().

    You've done all the work for me!

  8. eryksun commented on Mar 19, 2023

    @eryksun
    Contributor

    The issue also needs tests in "Lib/test_ntpath.py".

    is that function necessary in all four cases? I wonder if we can guarantee that path does or does not end with a separator in any of them.

    The result from nt._getfinalpathname() or nt.readlink() may or may not end with a separator. For prepending to tail, since name comes from ntpath.split(), it should never contain a backslash. However, tail could be empty, so we would need tail = (name + sep + tail) if tail else name, as opposed to tail = join(name, tail). I'd rather keep the check for an empty tail in join().

  9. johns1c commented on Sep 13, 2023

    @johns1c

    Is os.path.join just wrong?

    Is there a proper definition of a "canonical" path for Windows?

    If not should the Python community create a definition? It will have to takes account of namespaces, drive letters, mount points, case sensitivity etc .

    I think that one rule should be that If p2 is the result os.path.realpath( p1 ) then if a thing exists or can be created using path p2 it will be the same as that created / accessed via path p1.

    I also think that os.path.realpath( 'c:/aa.b' ) should give a similar result to os.path.realpath( 'c:/a.b' )

    p.s. see also some of the comments on issue #100162

  10. 2 remaining items

  11. added
    3.13only security fixes
    3.14bugs and security fixes
    3.15bugs and security fixes
    3.16new features, bugs and security fixes
    stdlibStandard Library Python modules in the Lib/ directory
    and removed
    3.11only security fixes
    3.12only security fixes
    on Aug 8, 2026
  12. added a commit that references this issue on Aug 29, 2026
  13. added 2 commits that reference this issue on Aug 29, 2026
  14. added a commit that references this issue on Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.13only security fixes3.14bugs and security fixes3.15bugs and security fixes3.16new features, bugs and security fixesOS-windowsstdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions