1919package pathrs
2020
2121import (
22+ "os"
23+ "path/filepath"
2224 "strings"
25+
26+ securejoin "github.com/cyphar/filepath-securejoin"
2327)
2428
2529// IsLexicallyInRoot is shorthand for strings.HasPrefix(path+"/", root+"/"),
@@ -32,3 +36,81 @@ func IsLexicallyInRoot(root, path string) bool {
3236 path = strings .TrimRight (path , "/" )
3337 return strings .HasPrefix (path + "/" , root + "/" )
3438}
39+
40+ // LexicallyCleanPath makes a path safe for use with filepath.Join. This is
41+ // done by not only cleaning the path, but also (if the path is relative)
42+ // adding a leading '/' and cleaning it (then removing the leading '/'). This
43+ // ensures that a path resulting from prepending another path will always
44+ // resolve to lexically be a subdirectory of the prefixed path. This is all
45+ // done lexically, so paths that include symlinks won't be safe as a result of
46+ // using CleanPath.
47+ func LexicallyCleanPath (path string ) string {
48+ // Deal with empty strings nicely.
49+ if path == "" {
50+ return ""
51+ }
52+
53+ // Ensure that all paths are cleaned (especially problematic ones like
54+ // "/../../../../../" which can cause lots of issues).
55+
56+ if filepath .IsAbs (path ) {
57+ return filepath .Clean (path )
58+ }
59+
60+ // If the path isn't absolute, we need to do more processing to fix paths
61+ // such as "../../../../<etc>/some/path". We also shouldn't convert absolute
62+ // paths to relative ones.
63+ path = filepath .Clean (string (os .PathSeparator ) + path )
64+ // This can't fail, as (by definition) all paths are relative to root.
65+ path , _ = filepath .Rel (string (os .PathSeparator ), path )
66+
67+ return path
68+ }
69+
70+ // LexicallyStripRoot returns the passed path, stripping the root path if it
71+ // was (lexicially) inside it. Note that both passed paths will always be
72+ // treated as absolute, and the returned path will also always be absolute. In
73+ // addition, the paths are cleaned before stripping the root.
74+ func LexicallyStripRoot (root , path string ) string {
75+ // Make the paths clean and absolute.
76+ root , path = LexicallyCleanPath ("/" + root ), LexicallyCleanPath ("/" + path )
77+ switch {
78+ case path == root :
79+ path = "/"
80+ case root == "/" :
81+ // do nothing
82+ default :
83+ path = strings .TrimPrefix (path , root + "/" )
84+ }
85+ return LexicallyCleanPath ("/" + path )
86+ }
87+
88+ // hallucinateUnsafePath creates a new unsafePath which has all symlinks
89+ // (including dangling symlinks) fully resolved and any non-existent components
90+ // treated as though they are real. This is effectively just a wrapper around
91+ // [securejoin.SecureJoin] that strips the root. This path *IS NOT* safe to use
92+ // as-is, you *MUST* operate on the returned path with pathrs-lite.
93+ //
94+ // The reason for this methods is that in previous runc versions, we would
95+ // tolerate nonsense paths with dangling symlinks as path components.
96+ // pathrs-lite does not support this, so instead we have to emulate this
97+ // behaviour by doing SecureJoin *purely to get a semi-reasonable path to use*
98+ // and then we use pathrs-lite to operate on the path safely.
99+ //
100+ // It would be quite difficult to emulate this in a race-free way in
101+ // pathrs-lite, so instead we use [securejoin.SecureJoin] to simply produce a
102+ // new candidate path for operations like [MkdirAllInRoot] so they can then
103+ // operate on the new unsafePath as if it was what the user requested.
104+ //
105+ // If unsafePath is already lexically inside root, it is stripped before
106+ // re-resolving it (this is done to ensure compatibility with legacy callers
107+ // within runc that call SecureJoin before calling into pathrs).
108+ func hallucinateUnsafePath (root , unsafePath string ) (string , error ) {
109+ unsafePath = LexicallyStripRoot (root , unsafePath )
110+ weirdPath , err := securejoin .SecureJoin (root , unsafePath )
111+ if err != nil {
112+ return "" , err
113+ }
114+ unsafePath = LexicallyStripRoot (root , weirdPath )
115+ return unsafePath , nil
116+ }
0 commit comments