33package utils
44
55import (
6+ "errors"
67 "fmt"
78 "math"
89 "os"
@@ -13,6 +14,8 @@ import (
1314 "sync"
1415 _ "unsafe" // for go:linkname
1516
17+ "github.com/opencontainers/runc/libcontainer/system"
18+
1619 securejoin "github.com/cyphar/filepath-securejoin"
1720 "github.com/sirupsen/logrus"
1821 "golang.org/x/sys/unix"
@@ -275,3 +278,112 @@ func IsLexicallyInRoot(root, path string) bool {
275278 }
276279 return strings .HasPrefix (path , root )
277280}
281+
282+ // MkdirAllInRootOpen attempts to make
283+ //
284+ // path, _ := securejoin.SecureJoin(root, unsafePath)
285+ // os.MkdirAll(path, mode)
286+ // os.Open(path)
287+ //
288+ // safer against attacks where components in the path are changed between
289+ // SecureJoin returning and MkdirAll (or Open) being called. In particular, we
290+ // try to detect any symlink components in the path while we are doing the
291+ // MkdirAll.
292+ //
293+ // NOTE: Unlike os.MkdirAll, mode is not Go's os.FileMode, it is the unix mode
294+ // (the suid/sgid/sticky bits are not the same as for os.FileMode).
295+ //
296+ // NOTE: If unsafePath is a subpath of root, we assume that you have already
297+ // called SecureJoin and so we use the provided path verbatim without resolving
298+ // any symlinks (this is done in a way that avoids symlink-exchange races).
299+ // This means that the path also must not contain ".." elements, otherwise an
300+ // error will occur.
301+ //
302+ // This is a somewhat less safe alternative to
303+ // <https://github.com/cyphar/filepath-securejoin/pull/13>, but it should
304+ // detect attempts to trick us into creating directories outside of the root.
305+ // We should migrate to securejoin.MkdirAll once it is merged.
306+ func MkdirAllInRootOpen (root , unsafePath string , mode uint32 ) (_ * os.File , Err error ) {
307+ // If the path is already "within" the root, use it verbatim.
308+ fullPath := unsafePath
309+ if ! IsLexicallyInRoot (root , unsafePath ) {
310+ var err error
311+ fullPath , err = securejoin .SecureJoin (root , unsafePath )
312+ if err != nil {
313+ return nil , err
314+ }
315+ }
316+ subPath , err := filepath .Rel (root , fullPath )
317+ if err != nil {
318+ return nil , err
319+ }
320+
321+ // Check for any silly mode bits.
322+ if mode &^0o7777 != 0 {
323+ return nil , fmt .Errorf ("tried to include non-mode bits in MkdirAll mode: 0o%.3o" , mode )
324+ }
325+
326+ currentDir , err := os .OpenFile (root , unix .O_DIRECTORY | unix .O_CLOEXEC , 0 )
327+ if err != nil {
328+ return nil , fmt .Errorf ("open root handle: %w" , err )
329+ }
330+ defer func () {
331+ if Err != nil {
332+ currentDir .Close ()
333+ }
334+ }()
335+
336+ for _ , part := range strings .Split (subPath , string (filepath .Separator )) {
337+ switch part {
338+ case "" , "." :
339+ // Skip over no-op components.
340+ continue
341+ case ".." :
342+ return nil , fmt .Errorf ("possible breakout detected: found %q component in SecureJoin subpath %s" , part , subPath )
343+ }
344+
345+ nextDir , err := system .Openat (currentDir , part , unix .O_DIRECTORY | unix .O_NOFOLLOW | unix .O_CLOEXEC , 0 )
346+ switch {
347+ case err == nil :
348+ // Update the currentDir.
349+ _ = currentDir .Close ()
350+ currentDir = nextDir
351+
352+ case errors .Is (err , unix .ENOTDIR ):
353+ // This might be a symlink or some other random file. Either way,
354+ // error out.
355+ return nil , fmt .Errorf ("cannot mkdir in %s/%s: %w" , currentDir .Name (), part , unix .ENOTDIR )
356+
357+ case errors .Is (err , os .ErrNotExist ):
358+ // Luckily, mkdirat will not follow trailing symlinks, so this is
359+ // safe to do as-is.
360+ if err := system .Mkdirat (currentDir , part , mode ); err != nil {
361+ return nil , err
362+ }
363+ // Open the new directory. There is a race here where an attacker
364+ // could swap the directory with a different directory, but
365+ // MkdirAll's fuzzy semantics mean we don't care about that.
366+ nextDir , err := system .Openat (currentDir , part , unix .O_DIRECTORY | unix .O_NOFOLLOW | unix .O_CLOEXEC , 0 )
367+ if err != nil {
368+ return nil , fmt .Errorf ("open newly created directory: %w" , err )
369+ }
370+ // Update the currentDir.
371+ _ = currentDir .Close ()
372+ currentDir = nextDir
373+
374+ default :
375+ return nil , err
376+ }
377+ }
378+ return currentDir , nil
379+ }
380+
381+ // MkdirAllInRoot is a wrapper around MkdirAllInRootOpen which closes the
382+ // returned handle, for callers that don't need to use it.
383+ func MkdirAllInRoot (root , unsafePath string , mode uint32 ) error {
384+ f , err := MkdirAllInRootOpen (root , unsafePath , mode )
385+ if err == nil {
386+ _ = f .Close ()
387+ }
388+ return err
389+ }
0 commit comments