@@ -436,3 +436,158 @@ func TestGetExecUIDGIDFromUser(t *testing.T) {
436436 })
437437 }
438438}
439+
440+ // newResolveTestVFS creates a tmpfs-backed VFS for resolve() tests.
441+ func newResolveTestVFS (t * testing.T ) (context.Context , * auth.Credentials , * vfs.VirtualFilesystem , * vfs.MountNamespace , vfs.VirtualDentry ) {
442+ t .Helper ()
443+ ctx := contexttest .Context (t )
444+ creds := auth .CredentialsFromContext (ctx )
445+
446+ vfsObj := & vfs.VirtualFilesystem {}
447+ if err := vfsObj .Init (ctx ); err != nil {
448+ t .Fatalf ("VFS init: %v" , err )
449+ }
450+ vfsObj .MustRegisterFilesystemType ("tmpfs" , tmpfs.FilesystemType {}, & vfs.RegisterFilesystemTypeOptions {
451+ AllowUserMount : true ,
452+ })
453+ mns , err := vfsObj .NewMountNamespace (ctx , creds , "" , "tmpfs" , & vfs.MountOptions {}, nil )
454+ if err != nil {
455+ t .Fatalf ("failed to create tmpfs root mount: %v" , err )
456+ }
457+ root := mns .Root (ctx )
458+ return ctx , creds , vfsObj , mns , root
459+ }
460+
461+ // createFile creates a regular file at the given path in the test VFS.
462+ func createFile (t * testing.T , ctx context.Context , creds * auth.Credentials , vfsObj * vfs.VirtualFilesystem , root vfs.VirtualDentry , filePath string , mode uint16 ) {
463+ t .Helper ()
464+ pop := vfs.PathOperation {
465+ Root : root ,
466+ Start : root ,
467+ Path : fspath .Parse (filePath ),
468+ }
469+ fd , err := vfsObj .OpenAt (ctx , creds , & pop , & vfs.OpenOptions {
470+ Flags : linux .O_CREAT | linux .O_WRONLY ,
471+ Mode : linux .S_IFREG | linux .FileMode (mode ),
472+ })
473+ if err != nil {
474+ t .Fatalf ("failed to create %s: %v" , filePath , err )
475+ }
476+ fd .DecRef (ctx )
477+ }
478+
479+ // createDir creates a directory at the given path in the test VFS.
480+ func createDir (t * testing.T , ctx context.Context , creds * auth.Credentials , vfsObj * vfs.VirtualFilesystem , root vfs.VirtualDentry , dirPath string ) {
481+ t .Helper ()
482+ pop := vfs.PathOperation {
483+ Root : root ,
484+ Start : root ,
485+ Path : fspath .Parse (dirPath ),
486+ }
487+ if err := vfsObj .MkdirAt (ctx , creds , & pop , & vfs.MkdirOptions {Mode : 0755 }); err != nil {
488+ t .Fatalf ("failed to create directory %s: %v" , dirPath , err )
489+ }
490+ }
491+
492+ // TestResolveExecutablePath tests that resolve() follows glibc execvpe()
493+ // semantics for handling errors during PATH search:
494+ // - ENOENT, ENOTDIR, ESTALE, ENODEV, ETIMEDOUT: skip to next PATH entry.
495+ // - EACCES: record and continue; if no executable is found, return EACCES
496+ // instead of ENOENT.
497+ // - Any other error: stop and return immediately.
498+ //
499+ // Reference: glibc posix/execvpe.c __execvpe_common().
500+ func TestResolveExecutablePath (t * testing.T ) {
501+ ctx , creds , vfsObj , mns , root := newResolveTestVFS (t )
502+ defer mns .DecRef (ctx )
503+ defer root .DecRef (ctx )
504+
505+ // Layout:
506+ // /not_a_dir — regular file (triggers ENOTDIR)
507+ // /bin/myexec — executable
508+ createFile (t , ctx , creds , vfsObj , root , "not_a_dir" , 0755 )
509+ createDir (t , ctx , creds , vfsObj , root , "bin" )
510+ createFile (t , ctx , creds , vfsObj , root , "bin/myexec" , 0755 )
511+
512+ t .Run ("SkipENOTDIR" , func (t * testing.T ) {
513+ // /not_a_dir/myexec → ENOTDIR, should skip and find /bin/myexec.
514+ got , err := resolve (ctx , creds , mns , []string {"/not_a_dir" , "/bin" }, "myexec" )
515+ if err != nil {
516+ t .Fatalf ("resolve failed: %v" , err )
517+ }
518+ if got != "/bin/myexec" {
519+ t .Fatalf ("expected /bin/myexec, got %v" , got )
520+ }
521+ })
522+
523+ t .Run ("SkipENOENT" , func (t * testing.T ) {
524+ // /nonexistent doesn't exist → ENOENT, should skip and find /bin/myexec.
525+ got , err := resolve (ctx , creds , mns , []string {"/nonexistent" , "/bin" }, "myexec" )
526+ if err != nil {
527+ t .Fatalf ("resolve failed: %v" , err )
528+ }
529+ if got != "/bin/myexec" {
530+ t .Fatalf ("expected /bin/myexec, got %v" , got )
531+ }
532+ })
533+
534+ t .Run ("AllMissReturnENOENT" , func (t * testing.T ) {
535+ // PATH has ENOTDIR then ENOENT. glibc preserves the last errno
536+ // from execve(), which is ENOENT from /nonexistent.
537+ _ , err := resolve (ctx , creds , mns , []string {"/not_a_dir" , "/nonexistent" }, "myexec" )
538+ if ! linuxerr .Equals (linuxerr .ENOENT , err ) {
539+ t .Fatalf ("expected ENOENT, got: %v" , err )
540+ }
541+ })
542+
543+ t .Run ("OnlyENOTDIR" , func (t * testing.T ) {
544+ // All PATH entries are files (not directories) → ENOTDIR.
545+ // glibc preserves the last errno, which is ENOTDIR.
546+ _ , err := resolve (ctx , creds , mns , []string {"/not_a_dir" }, "myexec" )
547+ if ! linuxerr .Equals (linuxerr .ENOTDIR , err ) {
548+ t .Fatalf ("expected ENOTDIR, got: %v" , err )
549+ }
550+ })
551+
552+ t .Run ("EACCESReturnedOverENOENT" , func (t * testing.T ) {
553+ // Create a file that exists but is not executable (mode 0644).
554+ // The first PATH entry will fail with EACCES, and the second
555+ // with ENOENT. Per glibc, EACCES should be returned.
556+ createDir (t , ctx , creds , vfsObj , root , "noperm" )
557+ createFile (t , ctx , creds , vfsObj , root , "noperm/myexec" , 0644 )
558+
559+ _ , err := resolve (ctx , creds , mns , []string {"/noperm" , "/nonexistent" }, "myexec" )
560+ if ! linuxerr .Equals (linuxerr .EACCES , err ) {
561+ t .Fatalf ("expected EACCES, got: %v" , err )
562+ }
563+ })
564+
565+ t .Run ("EACCESThenFound" , func (t * testing.T ) {
566+ // EACCES in first entry, but found in second → success.
567+ got , err := resolve (ctx , creds , mns , []string {"/noperm" , "/bin" }, "myexec" )
568+ if err != nil {
569+ t .Fatalf ("resolve failed: %v" , err )
570+ }
571+ if got != "/bin/myexec" {
572+ t .Fatalf ("expected /bin/myexec, got %v" , got )
573+ }
574+ })
575+
576+ t .Run ("EmptyPATH" , func (t * testing.T ) {
577+ _ , err := resolve (ctx , creds , mns , nil , "myexec" )
578+ if ! linuxerr .Equals (linuxerr .ENOENT , err ) {
579+ t .Fatalf ("expected ENOENT, got: %v" , err )
580+ }
581+ })
582+
583+ t .Run ("SkipRelativePath" , func (t * testing.T ) {
584+ // Relative paths should be skipped.
585+ got , err := resolve (ctx , creds , mns , []string {"relative" , "/bin" }, "myexec" )
586+ if err != nil {
587+ t .Fatalf ("resolve failed: %v" , err )
588+ }
589+ if got != "/bin/myexec" {
590+ t .Fatalf ("expected /bin/myexec, got %v" , got )
591+ }
592+ })
593+ }
0 commit comments