mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
Align resolve() error handling with glibc execvpe() (posix/execvpe.c): - Skip ENOENT, ENOTDIR, ESTALE, ENODEV, ETIMEDOUT and continue to the next PATH entry, matching glibc's switch(errno) behavior. - Track EACCES separately: if all PATH entries fail and at least one returned EACCES, report EACCES instead of ENOENT, matching glibc's got_eacces logic. - Preserve the last error from execve() when no entry works, matching glibc's behavior of not resetting errno after the loop. For example, if all PATH entries are regular files (ENOTDIR), return ENOTDIR instead of ENOENT. - Any other error is returned immediately. Previously only ENOENT and EACCES were skipped, and ENOENT was always returned as the fallback error regardless of the actual last failure. Test: TestResolveExecutablePath Signed-off-by: Tan Yifeng <[email protected]>
594 lines
19 KiB
Go
594 lines
19 KiB
Go
// Copyright 2019 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package user
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gvisor.dev/gvisor/pkg/abi/linux"
|
|
"gvisor.dev/gvisor/pkg/context"
|
|
"gvisor.dev/gvisor/pkg/errors/linuxerr"
|
|
"gvisor.dev/gvisor/pkg/fspath"
|
|
"gvisor.dev/gvisor/pkg/sentry/fsimpl/tmpfs"
|
|
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
|
|
"gvisor.dev/gvisor/pkg/sentry/kernel/contexttest"
|
|
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
|
"gvisor.dev/gvisor/pkg/usermem"
|
|
)
|
|
|
|
// createEtcFile creates /etc/<file> with the given contents and mode. If
|
|
// mode is empty, then no file will be created. If mode is not a regular file
|
|
// mode, then contents is ignored.
|
|
func createEtcFile(ctx context.Context, vfsObj *vfs.VirtualFilesystem, creds *auth.Credentials, root vfs.VirtualDentry, contents string, mode linux.FileMode, filePath string) error {
|
|
pop := vfs.PathOperation{
|
|
Root: root,
|
|
Start: root,
|
|
Path: fspath.Parse("etc"),
|
|
}
|
|
if err := vfsObj.MkdirAt(ctx, creds, &pop, &vfs.MkdirOptions{
|
|
Mode: 0755,
|
|
}); err != nil && err != linuxerr.EEXIST { // Ignore error if it already exists.
|
|
return fmt.Errorf("failed to create directory etc: %v", err)
|
|
}
|
|
|
|
pop = vfs.PathOperation{
|
|
Root: root,
|
|
Start: root,
|
|
Path: fspath.Parse(filePath),
|
|
}
|
|
switch mode.FileType() {
|
|
case 0:
|
|
// Don't create anything.
|
|
return nil
|
|
case linux.S_IFREG:
|
|
fd, err := vfsObj.OpenAt(ctx, creds, &pop, &vfs.OpenOptions{Flags: linux.O_CREAT | linux.O_WRONLY, Mode: mode})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fd.DecRef(ctx)
|
|
_, err = fd.Write(ctx, usermem.BytesIOSequence([]byte(contents)), vfs.WriteOptions{})
|
|
return err
|
|
case linux.S_IFDIR:
|
|
return vfsObj.MkdirAt(ctx, creds, &pop, &vfs.MkdirOptions{Mode: mode})
|
|
case linux.S_IFIFO:
|
|
return vfsObj.MknodAt(ctx, creds, &pop, &vfs.MknodOptions{Mode: mode})
|
|
default:
|
|
return fmt.Errorf("unknown file type %x", mode.FileType())
|
|
}
|
|
}
|
|
|
|
// TestGetExecUserHome tests the getExecUserHome function.
|
|
func TestGetExecUserHome(t *testing.T) {
|
|
tests := map[string]struct {
|
|
uid auth.KUID
|
|
passwdContents string
|
|
passwdMode linux.FileMode
|
|
expected string
|
|
}{
|
|
"success": {
|
|
uid: 1000,
|
|
passwdContents: "adin::1000:1111::/home/adin:/bin/sh",
|
|
passwdMode: linux.S_IFREG | 0666,
|
|
expected: "/home/adin",
|
|
},
|
|
"no_perms": {
|
|
uid: 1000,
|
|
passwdContents: "adin::1000:1111::/home/adin:/bin/sh",
|
|
passwdMode: linux.S_IFREG,
|
|
expected: "/",
|
|
},
|
|
"no_passwd": {
|
|
uid: 1000,
|
|
expected: "/",
|
|
},
|
|
"directory": {
|
|
uid: 1000,
|
|
passwdMode: linux.S_IFDIR | 0666,
|
|
expected: "/",
|
|
},
|
|
// Currently we don't allow named pipes.
|
|
"named_pipe": {
|
|
uid: 1000,
|
|
passwdMode: linux.S_IFIFO | 0666,
|
|
expected: "/",
|
|
},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
ctx := contexttest.Context(t)
|
|
creds := auth.CredentialsFromContext(ctx)
|
|
|
|
// Create VFS.
|
|
vfsObj := vfs.VirtualFilesystem{}
|
|
if err := vfsObj.Init(ctx); err != nil {
|
|
t.Fatalf("VFS init: %v", err)
|
|
}
|
|
vfsObj.MustRegisterFilesystemType("tmpfs", tmpfs.FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{
|
|
AllowUserMount: true,
|
|
})
|
|
mns, err := vfsObj.NewMountNamespace(ctx, creds, "", "tmpfs", &vfs.MountOptions{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to create tmpfs root mount: %v", err)
|
|
}
|
|
defer mns.DecRef(ctx)
|
|
root := mns.Root(ctx)
|
|
defer root.DecRef(ctx)
|
|
|
|
if err := createEtcFile(ctx, &vfsObj, creds, root, tc.passwdContents, tc.passwdMode, "/etc/passwd"); err != nil {
|
|
t.Fatalf("createEtcPasswd failed: %v", err)
|
|
}
|
|
|
|
got, err := getExecUserHome(ctx, mns, tc.uid)
|
|
if err != nil {
|
|
t.Fatalf("failed to get user home: %v", err)
|
|
}
|
|
|
|
if got != tc.expected {
|
|
t.Fatalf("expected %v, got: %v", tc.expected, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFindHomeInPasswd tests the findHomeInPasswd function's passwd file parsing.
|
|
func TestFindHomeInPasswd(t *testing.T) {
|
|
tests := map[string]struct {
|
|
uid uint32
|
|
passwd string
|
|
expected string
|
|
def string
|
|
}{
|
|
"empty": {
|
|
uid: 1000,
|
|
passwd: "",
|
|
expected: "/",
|
|
def: "/",
|
|
},
|
|
"whitespace": {
|
|
uid: 1000,
|
|
passwd: " ",
|
|
expected: "/",
|
|
def: "/",
|
|
},
|
|
"full": {
|
|
uid: 1000,
|
|
passwd: "adin::1000:1111::/home/adin:/bin/sh",
|
|
expected: "/home/adin",
|
|
def: "/",
|
|
},
|
|
// For better or worse, this is how runc works.
|
|
"partial": {
|
|
uid: 1000,
|
|
passwd: "adin::1000:1111:",
|
|
expected: "",
|
|
def: "/",
|
|
},
|
|
"multiple": {
|
|
uid: 1001,
|
|
passwd: "adin::1000:1111::/home/adin:/bin/sh\nian::1001:1111::/home/ian:/bin/sh",
|
|
expected: "/home/ian",
|
|
def: "/",
|
|
},
|
|
"duplicate": {
|
|
uid: 1000,
|
|
passwd: "adin::1000:1111::/home/adin:/bin/sh\nian::1000:1111::/home/ian:/bin/sh",
|
|
expected: "/home/adin",
|
|
def: "/",
|
|
},
|
|
"empty_lines": {
|
|
uid: 1001,
|
|
passwd: "adin::1000:1111::/home/adin:/bin/sh\n\n\nian::1001:1111::/home/ian:/bin/sh",
|
|
expected: "/home/ian",
|
|
def: "/",
|
|
},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
got, err := findHomeInPasswd(tc.uid, strings.NewReader(tc.passwd), tc.def)
|
|
if err != nil {
|
|
t.Fatalf("error parsing passwd: %v", err)
|
|
}
|
|
if tc.expected != got {
|
|
t.Fatalf("expected %v, got: %v", tc.expected, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetExecUIDGIDFromUser tests the GetExecUIDGIDFromUser function.
|
|
func TestGetExecUIDGIDFromUser(t *testing.T) {
|
|
tests := map[string]struct {
|
|
user string
|
|
passwdContents string
|
|
groupContents string
|
|
fileMode linux.FileMode
|
|
expectedUID auth.KUID
|
|
expectedGID auth.KGID
|
|
}{
|
|
"user0": {
|
|
user: "user0",
|
|
passwdContents: "user0::1000:1111:&:/home/user0:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1000,
|
|
expectedGID: 1111,
|
|
},
|
|
"uid_only": {
|
|
user: "1000",
|
|
passwdContents: "user0::1000:1111:&:/home/user0:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1000,
|
|
expectedGID: 1111,
|
|
},
|
|
"uid_and_gid": {
|
|
user: "1000:1111",
|
|
passwdContents: "user0::1000:1111:&:/home/user0:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1000,
|
|
expectedGID: 1111,
|
|
},
|
|
"uid_and_wrong_gid": {
|
|
user: "1000:1112",
|
|
passwdContents: "user0::1000:1111:&:/home/user0:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1000,
|
|
expectedGID: 1112,
|
|
},
|
|
"user_missing": {
|
|
user: "user1",
|
|
passwdContents: "user0::1000:1111::/home/user0:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 0,
|
|
expectedGID: 0,
|
|
},
|
|
"multiple_user_no_match": {
|
|
user: "user1",
|
|
passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser2::1002:1112::/home/user2:/bin/sh\nuser3::1003:1113::/home/user3:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 0,
|
|
expectedGID: 0,
|
|
},
|
|
"multiple_user_many_match": {
|
|
user: "user1",
|
|
passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser1::1002:1112::/home/user1:/bin/sh\nuser1::1003:1113::/home/user1:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1002,
|
|
expectedGID: 1112,
|
|
},
|
|
"invalid_file": {
|
|
user: "user1",
|
|
passwdContents: "user0:1000:1111::/home/user0:/bin/sh\nuser1::1001:1111::/home/user1:/bin/sh\nuser2::/home/user2:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 0,
|
|
expectedGID: 0,
|
|
},
|
|
"empty_file": {
|
|
user: "user1",
|
|
passwdContents: "",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 0,
|
|
expectedGID: 0,
|
|
},
|
|
"empty_user": {
|
|
user: "",
|
|
passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser2::1002:1112::/home/user2:/bin/sh\nuser3::1003:1113::/home/user3:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 0,
|
|
expectedGID: 0,
|
|
},
|
|
"with_comments": {
|
|
user: "user0",
|
|
passwdContents: "#This is a comment\nuser0::1000:1111:&:/home/user0:/bin/sh\nuser2::1002:1112:&:/home/user2:/bin/sh\nuser3:&:1003:1113::/home/user3:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1000,
|
|
expectedGID: 1111,
|
|
},
|
|
"with_comments_mid_file": {
|
|
user: "user0",
|
|
passwdContents: "user0::1000:1111:&:/home/user0:/bin/sh\nuser2::1002:1112:&:/home/user2:/bin/sh\n#This is a comment\n\nuser3:&:1003:1113::/home/user3:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1000,
|
|
expectedGID: 1111,
|
|
},
|
|
"empty_gecos": {
|
|
user: "user0",
|
|
passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser2::1002:1112::/home/user2:/bin/sh\nuser3::1003:1113::/home/user3:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1000,
|
|
expectedGID: 1111,
|
|
},
|
|
"numeric_user_empty_file": {
|
|
user: "1000",
|
|
passwdContents: "",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1000,
|
|
expectedGID: 0,
|
|
},
|
|
"numeric_user_does_not_match": {
|
|
user: "1005",
|
|
passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser2::1002:1112::/home/user2:/bin/sh\nuser3::1003:1113::/home/user3:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1005,
|
|
expectedGID: 0,
|
|
},
|
|
"numeric_user_numeric_group_does_not_match": {
|
|
user: "1005:1006",
|
|
passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser2::1002:1112::/home/user2:/bin/sh\nuser3::1003:1113::/home/user3:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1005,
|
|
expectedGID: 1006,
|
|
},
|
|
"numeric_user_string_group_does_not_match": {
|
|
user: "1005:group10",
|
|
passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser2::1002:1112::/home/user2:/bin/sh\nuser3::1003:1113::/home/user3:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1005,
|
|
expectedGID: 0,
|
|
},
|
|
"string_user_numeric_group_does_not_match": {
|
|
user: "user10:1006",
|
|
passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser2::1002:1112::/home/user2:/bin/sh\nuser3::1003:1113::/home/user3:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 0,
|
|
expectedGID: 1006,
|
|
},
|
|
"string_user_string_group_does_not_match": {
|
|
user: "user10:group10",
|
|
passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser2::1002:1112::/home/user2:/bin/sh\nuser3::1003:1113::/home/user3:/bin/sh",
|
|
groupContents: "",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 0,
|
|
expectedGID: 0,
|
|
},
|
|
"group_does_not_match_but_exist": {
|
|
user: "user0:group2",
|
|
passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser2::1002:1112::/home/user2:/bin/sh\nuser3::1003:1113::/home/user3:/bin/sh",
|
|
groupContents: "group1:x:1110:user0,user1,user2\ngroup2:x:1112:user0,user1,user2",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1000,
|
|
expectedGID: 1112,
|
|
},
|
|
"group_does_not_match_and_does_not_exist": {
|
|
user: "user0:group10",
|
|
passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser2::1002:1112::/home/user2:/bin/sh\nuser3::1003:1113::/home/user3:/bin/sh",
|
|
groupContents: "group1:x:1110:user0,user1,user2\ngroup2:x:1112:user0,user1,user2",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1000,
|
|
expectedGID: 0,
|
|
},
|
|
"numeric_group_does_not_match_and_does_not_exist": {
|
|
user: "user0:1113",
|
|
passwdContents: "user0::1000:1111::/home/user0:/bin/sh\nuser2::1002:1112::/home/user2:/bin/sh\nuser3::1003:1113::/home/user3:/bin/sh",
|
|
groupContents: "group1:x:1110:user0,user1,user2\ngroup2:x:1112:user0,user1,user2",
|
|
fileMode: linux.S_IFREG | 0666,
|
|
expectedUID: 1000,
|
|
expectedGID: 1113,
|
|
}}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
ctx := contexttest.Context(t)
|
|
creds := auth.CredentialsFromContext(ctx)
|
|
|
|
// Create VFS.
|
|
vfsObj := vfs.VirtualFilesystem{}
|
|
if err := vfsObj.Init(ctx); err != nil {
|
|
t.Fatalf("VFS init: %v", err)
|
|
}
|
|
vfsObj.MustRegisterFilesystemType("tmpfs", tmpfs.FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{
|
|
AllowUserMount: true,
|
|
})
|
|
mns, err := vfsObj.NewMountNamespace(ctx, creds, "", "tmpfs", &vfs.MountOptions{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to create tmpfs root mount: %v", err)
|
|
}
|
|
defer mns.DecRef(ctx)
|
|
root := mns.Root(ctx)
|
|
defer root.DecRef(ctx)
|
|
|
|
if err := createEtcFile(ctx, &vfsObj, creds, root, tc.passwdContents, tc.fileMode, "etc/passwd"); err != nil {
|
|
t.Fatalf("createEtcFile failed: %v", err)
|
|
}
|
|
if err := createEtcFile(ctx, &vfsObj, creds, root, tc.groupContents, tc.fileMode, "/etc/group"); err != nil {
|
|
t.Fatalf("createEtcFile failed: %v", err)
|
|
}
|
|
|
|
gotUID, gotGID := GetExecUIDGIDFromUser(ctx, mns, tc.user)
|
|
if gotUID != tc.expectedUID {
|
|
t.Fatalf("expectedUID %v, gotUID: %v", tc.expectedUID, gotUID)
|
|
}
|
|
if gotGID != tc.expectedGID {
|
|
t.Fatalf("expectedGID %v, gotGID: %v", tc.expectedGID, gotGID)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// newResolveTestVFS creates a tmpfs-backed VFS for resolve() tests.
|
|
func newResolveTestVFS(t *testing.T) (context.Context, *auth.Credentials, *vfs.VirtualFilesystem, *vfs.MountNamespace, vfs.VirtualDentry) {
|
|
t.Helper()
|
|
ctx := contexttest.Context(t)
|
|
creds := auth.CredentialsFromContext(ctx)
|
|
|
|
vfsObj := &vfs.VirtualFilesystem{}
|
|
if err := vfsObj.Init(ctx); err != nil {
|
|
t.Fatalf("VFS init: %v", err)
|
|
}
|
|
vfsObj.MustRegisterFilesystemType("tmpfs", tmpfs.FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{
|
|
AllowUserMount: true,
|
|
})
|
|
mns, err := vfsObj.NewMountNamespace(ctx, creds, "", "tmpfs", &vfs.MountOptions{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to create tmpfs root mount: %v", err)
|
|
}
|
|
root := mns.Root(ctx)
|
|
return ctx, creds, vfsObj, mns, root
|
|
}
|
|
|
|
// createFile creates a regular file at the given path in the test VFS.
|
|
func createFile(t *testing.T, ctx context.Context, creds *auth.Credentials, vfsObj *vfs.VirtualFilesystem, root vfs.VirtualDentry, filePath string, mode uint16) {
|
|
t.Helper()
|
|
pop := vfs.PathOperation{
|
|
Root: root,
|
|
Start: root,
|
|
Path: fspath.Parse(filePath),
|
|
}
|
|
fd, err := vfsObj.OpenAt(ctx, creds, &pop, &vfs.OpenOptions{
|
|
Flags: linux.O_CREAT | linux.O_WRONLY,
|
|
Mode: linux.S_IFREG | linux.FileMode(mode),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to create %s: %v", filePath, err)
|
|
}
|
|
fd.DecRef(ctx)
|
|
}
|
|
|
|
// createDir creates a directory at the given path in the test VFS.
|
|
func createDir(t *testing.T, ctx context.Context, creds *auth.Credentials, vfsObj *vfs.VirtualFilesystem, root vfs.VirtualDentry, dirPath string) {
|
|
t.Helper()
|
|
pop := vfs.PathOperation{
|
|
Root: root,
|
|
Start: root,
|
|
Path: fspath.Parse(dirPath),
|
|
}
|
|
if err := vfsObj.MkdirAt(ctx, creds, &pop, &vfs.MkdirOptions{Mode: 0755}); err != nil {
|
|
t.Fatalf("failed to create directory %s: %v", dirPath, err)
|
|
}
|
|
}
|
|
|
|
// TestResolveExecutablePath tests that resolve() follows glibc execvpe()
|
|
// semantics for handling errors during PATH search:
|
|
// - ENOENT, ENOTDIR, ESTALE, ENODEV, ETIMEDOUT: skip to next PATH entry.
|
|
// - EACCES: record and continue; if no executable is found, return EACCES
|
|
// instead of ENOENT.
|
|
// - Any other error: stop and return immediately.
|
|
//
|
|
// Reference: glibc posix/execvpe.c __execvpe_common().
|
|
func TestResolveExecutablePath(t *testing.T) {
|
|
ctx, creds, vfsObj, mns, root := newResolveTestVFS(t)
|
|
defer mns.DecRef(ctx)
|
|
defer root.DecRef(ctx)
|
|
|
|
// Layout:
|
|
// /not_a_dir — regular file (triggers ENOTDIR)
|
|
// /bin/myexec — executable
|
|
createFile(t, ctx, creds, vfsObj, root, "not_a_dir", 0755)
|
|
createDir(t, ctx, creds, vfsObj, root, "bin")
|
|
createFile(t, ctx, creds, vfsObj, root, "bin/myexec", 0755)
|
|
|
|
t.Run("SkipENOTDIR", func(t *testing.T) {
|
|
// /not_a_dir/myexec → ENOTDIR, should skip and find /bin/myexec.
|
|
got, err := resolve(ctx, creds, mns, []string{"/not_a_dir", "/bin"}, "myexec")
|
|
if err != nil {
|
|
t.Fatalf("resolve failed: %v", err)
|
|
}
|
|
if got != "/bin/myexec" {
|
|
t.Fatalf("expected /bin/myexec, got %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("SkipENOENT", func(t *testing.T) {
|
|
// /nonexistent doesn't exist → ENOENT, should skip and find /bin/myexec.
|
|
got, err := resolve(ctx, creds, mns, []string{"/nonexistent", "/bin"}, "myexec")
|
|
if err != nil {
|
|
t.Fatalf("resolve failed: %v", err)
|
|
}
|
|
if got != "/bin/myexec" {
|
|
t.Fatalf("expected /bin/myexec, got %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("AllMissReturnENOENT", func(t *testing.T) {
|
|
// PATH has ENOTDIR then ENOENT. glibc preserves the last errno
|
|
// from execve(), which is ENOENT from /nonexistent.
|
|
_, err := resolve(ctx, creds, mns, []string{"/not_a_dir", "/nonexistent"}, "myexec")
|
|
if !linuxerr.Equals(linuxerr.ENOENT, err) {
|
|
t.Fatalf("expected ENOENT, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("OnlyENOTDIR", func(t *testing.T) {
|
|
// All PATH entries are files (not directories) → ENOTDIR.
|
|
// glibc preserves the last errno, which is ENOTDIR.
|
|
_, err := resolve(ctx, creds, mns, []string{"/not_a_dir"}, "myexec")
|
|
if !linuxerr.Equals(linuxerr.ENOTDIR, err) {
|
|
t.Fatalf("expected ENOTDIR, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("EACCESReturnedOverENOENT", func(t *testing.T) {
|
|
// Create a file that exists but is not executable (mode 0644).
|
|
// The first PATH entry will fail with EACCES, and the second
|
|
// with ENOENT. Per glibc, EACCES should be returned.
|
|
createDir(t, ctx, creds, vfsObj, root, "noperm")
|
|
createFile(t, ctx, creds, vfsObj, root, "noperm/myexec", 0644)
|
|
|
|
_, err := resolve(ctx, creds, mns, []string{"/noperm", "/nonexistent"}, "myexec")
|
|
if !linuxerr.Equals(linuxerr.EACCES, err) {
|
|
t.Fatalf("expected EACCES, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("EACCESThenFound", func(t *testing.T) {
|
|
// EACCES in first entry, but found in second → success.
|
|
got, err := resolve(ctx, creds, mns, []string{"/noperm", "/bin"}, "myexec")
|
|
if err != nil {
|
|
t.Fatalf("resolve failed: %v", err)
|
|
}
|
|
if got != "/bin/myexec" {
|
|
t.Fatalf("expected /bin/myexec, got %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("EmptyPATH", func(t *testing.T) {
|
|
_, err := resolve(ctx, creds, mns, nil, "myexec")
|
|
if !linuxerr.Equals(linuxerr.ENOENT, err) {
|
|
t.Fatalf("expected ENOENT, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("SkipRelativePath", func(t *testing.T) {
|
|
// Relative paths should be skipped.
|
|
got, err := resolve(ctx, creds, mns, []string{"relative", "/bin"}, "myexec")
|
|
if err != nil {
|
|
t.Fatalf("resolve failed: %v", err)
|
|
}
|
|
if got != "/bin/myexec" {
|
|
t.Fatalf("expected /bin/myexec, got %v", got)
|
|
}
|
|
})
|
|
}
|