diff --git a/internal/winapi/offline_registry_windows.go b/internal/winapi/offline_registry_windows.go new file mode 100644 index 000000000000..a0ca1aa9ad22 --- /dev/null +++ b/internal/winapi/offline_registry_windows.go @@ -0,0 +1,17 @@ +package winapi + +const ( + REG_OPTION_CREATE_LINK uint32 = 0x00000002 + REG_OPTION_NON_VOLATILE uint32 = 0x00000000 + MAX_KEY_NAME uint32 = 255 + MAX_VALUE_NAME uint32 = 16383 +) + +//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go ./*.go +//sys OROpenHive(file *uint16, key *syscall.Handle) (regerrno error) = offreg.OROpenHive +//sys ORCloseHive(key *syscall.Handle) (regerrno error) = offreg.ORCloseHive +//sys OROpenKey(rootKey syscall.Handle, lpSubKeyName *uint16, key *syscall.Handle) (regerrno error) = offreg.OROpenKey +//sys ORCloseKey(key *syscall.Handle) (regerrno error) = offreg.ORCloseKey +//sys OREnumKey(key syscall.Handle, index uint32, name *uint16, nameSize *uint32, class *uint16, classSize *uint32, ftLastWriteTime uintptr) (regerrno error) = offreg.OREnumKey +//sys OREnumValue(key syscall.Handle, index uint32, valueName *uint16, valueNameSize *uint32, valueType *uint32, data *byte, bufferSize *uint32) (regerrno error) = offreg.OREnumValue +//sys ORQueryInfoKey(key syscall.Handle, lpClass *uint16, lpcClass *uint32, lpcSubKeys *uint32, lpcMaxSubKeyLen *uint32, lpcMaxClassLen *uint32, lpcValues *uint32, lpcMaxValueNameLen *uint32, lpcMaxValueLen *uint32, lpcbSecurityDescriptor *uint32, lpftLastWriteTime uintptr) (regerrno error) = offreg.ORQueryInfoKey diff --git a/internal/winapi/sam_windows.go b/internal/winapi/sam_windows.go new file mode 100644 index 000000000000..e658c0f2a4ea --- /dev/null +++ b/internal/winapi/sam_windows.go @@ -0,0 +1,206 @@ +package winapi + +import ( + "bytes" + "encoding/binary" + "fmt" + "strconv" + "strings" + "syscall" + "unicode/utf16" + + "golang.org/x/sys/windows" +) + +func decodeEntry(buffer []byte) (string, error) { + name := make([]uint16, len(buffer)/2) + err := binary.Read(bytes.NewReader(buffer), binary.LittleEndian, &name) + if err != nil { + return "", fmt.Errorf("decoding name: %w", err) + } + return string(utf16.Decode(name)), nil +} + +type samValue struct { + Name string + Data []byte +} + +type SAMUser struct { + Username string + SIDString string + RID int64 +} + +func reverse(s []byte) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} + +func (s *SAMUser) SID() (*windows.SID, error) { + if s.SIDString == "" { + return nil, fmt.Errorf("no sid available") + } + utfPtr, err := syscall.UTF16PtrFromString(s.SIDString) + if err != nil { + return nil, fmt.Errorf("converting string to utf-16 ptr: %w", err) + } + var sid *windows.SID + if err := windows.ConvertStringSidToSid(utfPtr, &sid); err != nil { + return nil, fmt.Errorf("fetching SID: %w", err) + } + return sid, nil +} + +func getValues(key syscall.Handle) ([]samValue, error) { + var subKeys uint32 + var nValues uint32 + if err := ORQueryInfoKey(key, nil, nil, &subKeys, nil, nil, &nValues, nil, nil, nil, 0); err != nil { + return nil, err + } + + ret := make([]samValue, nValues) + + var j uint32 = 0 + for j = 0; j < nValues; j++ { + nSize := MAX_VALUE_NAME + var dwType uint32 = 0 + var cbData uint32 = 0 + valueName := make([]uint16, nSize) + if err := OREnumValue(key, j, &valueName[0], &nSize, &dwType, nil, &cbData); err != syscall.ERROR_MORE_DATA { + continue + } + buffer := make([]byte, cbData) + + if err := OREnumValue(key, j, &valueName[0], &nSize, &dwType, &buffer[0], &cbData); err != nil { + return nil, err + } + valueNameString := syscall.UTF16ToString(valueName[:nSize]) + ret[j] = samValue{ + Name: valueNameString, + Data: buffer, + } + } + return ret, nil +} + +func parseUserInfo(data samValue, rid int64) (SAMUser, error) { + usernameOffset := binary.LittleEndian.Uint32(data.Data[12:16]) + usernameLen := binary.LittleEndian.Uint32(data.Data[16:20]) + ret, err := decodeEntry(data.Data[usernameOffset+0xCC : (usernameOffset+0xCC)+usernameLen]) + if err != nil { + return SAMUser{}, fmt.Errorf("decoding username: %w", err) + } + // Before the username offset, we have the Administrators group SID, twice. + // The user full SID starts 16 bytes before the two Administrator group SIDs. + // Skip 32 bytes, then fetch preceding 16 bytes we care about. + // See https://web.archive.org/web/20190423074618/http://www.beginningtoseethelight.org:80/ntsecurity/index.htm + // Sadly, the archived link is the best description of the format of the SAM hive + // I have been able to find. + sidBytes := data.Data[(usernameOffset+0xcc)-48 : (usernameOffset+0xcc)-32] + first := sidBytes[0:4] + second := sidBytes[4:8] + third := sidBytes[8:12] + fourth := sidBytes[12:16] + // fmt.Println(first, second, third, fourth) + reverse(first) + reverse(second) + reverse(third) + reverse(fourth) + foundRid := binary.BigEndian.Uint32(fourth) + if foundRid != uint32(rid) { + return SAMUser{}, nil + } + sid := fmt.Sprintf("S-1-5-21-%d-%d-%d-%d", binary.BigEndian.Uint32(first), binary.BigEndian.Uint32(second), binary.BigEndian.Uint32(third), binary.BigEndian.Uint32(fourth)) + return SAMUser{ + Username: ret, + SIDString: sid, + RID: rid, + }, nil +} + +func walkSAMUsers(rootKey syscall.Handle) ([]SAMUser, error) { + subkeyName, err := syscall.UTF16PtrFromString("sam\\Domains\\Account\\Users") + if err != nil { + return nil, fmt.Errorf("fetching utf16 pointer: %w", err) + } + + var users syscall.Handle + if err := OROpenKey(rootKey, subkeyName, &users); err != nil { + return nil, fmt.Errorf("opening users key: %w", err) + } + defer ORCloseKey(&users) + + var subkeys uint32 + var nvalues uint32 + if err := ORQueryInfoKey(users, nil, nil, &subkeys, nil, nil, &nvalues, nil, nil, nil, 0); err != nil { + return nil, fmt.Errorf("querying key info: %w", err) + } + + SAMUsers := []SAMUser{} + + var key uint32 + for key = 0; key < subkeys; key++ { + nSize := MAX_KEY_NAME + name := make([]uint16, nSize) + if err := OREnumKey(users, key, &name[0], &nSize, nil, nil, 0); err != nil { + return nil, fmt.Errorf("enumerating key: %w", err) + } + keyName := syscall.UTF16ToString(name[:nSize]) + if strings.EqualFold(keyName, "names") { + continue + } + + rid, err := strconv.ParseInt(keyName, 16, 64) + if err != nil { + return nil, fmt.Errorf("parsing RID: %w", err) + } + + data := name[:nSize] + var userKey syscall.Handle + if err := OROpenKey(users, &data[0], &userKey); err != nil { + return nil, fmt.Errorf("opening key: %w", err) + } + + values, err := getValues(userKey) + if err != nil { + return nil, fmt.Errorf("fetching values for key: %w", err) + } + + for _, val := range values { + if val.Name == "V" { + parsed, err := parseUserInfo(val, rid) + if err != nil { + return nil, fmt.Errorf("parsing user data: %w", err) + } + if parsed.SIDString == "" { + continue + } + SAMUsers = append(SAMUsers, parsed) + } + } + + } + + return SAMUsers, nil +} + +func GetUserInfoFromOfflineSAMHive(samPath string) ([]SAMUser, error) { + hivePath, err := syscall.UTF16PtrFromString("C:\\Users\\Administrator\\work\\getuser\\sam2") + if err != nil { + return nil, fmt.Errorf("getting path: %v", err) + } + + var key syscall.Handle + if err := OROpenHive(hivePath, &key); err != nil { + return nil, fmt.Errorf("opening hive: %v", err) + } + defer ORCloseHive(&key) + + users, err := walkSAMUsers(key) + if err != nil { + return nil, fmt.Errorf("parsing hive: %v", err) + } + return users, nil +} diff --git a/internal/winapi/winapi_windows.go b/internal/winapi/winapi_windows.go new file mode 100644 index 000000000000..6a90e3a69ac9 --- /dev/null +++ b/internal/winapi/winapi_windows.go @@ -0,0 +1,3 @@ +package winapi + +//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go ./*.go diff --git a/internal/winapi/zsyscall_windows.go b/internal/winapi/zsyscall_windows.go new file mode 100644 index 000000000000..d73ddd60c26e --- /dev/null +++ b/internal/winapi/zsyscall_windows.go @@ -0,0 +1,108 @@ +//go:build windows + +// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. + +package winapi + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) + errERROR_EINVAL error = syscall.EINVAL +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return errERROR_EINVAL + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modoffreg = windows.NewLazySystemDLL("offreg.dll") + + procORCloseHive = modoffreg.NewProc("ORCloseHive") + procORCloseKey = modoffreg.NewProc("ORCloseKey") + procOREnumKey = modoffreg.NewProc("OREnumKey") + procOREnumValue = modoffreg.NewProc("OREnumValue") + procOROpenHive = modoffreg.NewProc("OROpenHive") + procOROpenKey = modoffreg.NewProc("OROpenKey") + procORQueryInfoKey = modoffreg.NewProc("ORQueryInfoKey") +) + +func ORCloseHive(key *syscall.Handle) (regerrno error) { + r0, _, _ := syscall.Syscall(procORCloseHive.Addr(), 1, uintptr(unsafe.Pointer(key)), 0, 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func ORCloseKey(key *syscall.Handle) (regerrno error) { + r0, _, _ := syscall.Syscall(procORCloseKey.Addr(), 1, uintptr(unsafe.Pointer(key)), 0, 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func OREnumKey(key syscall.Handle, index uint32, name *uint16, nameSize *uint32, class *uint16, classSize *uint32, ftLastWriteTime uintptr) (regerrno error) { + r0, _, _ := syscall.Syscall9(procOREnumKey.Addr(), 7, uintptr(key), uintptr(index), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(nameSize)), uintptr(unsafe.Pointer(class)), uintptr(unsafe.Pointer(classSize)), uintptr(ftLastWriteTime), 0, 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func OREnumValue(key syscall.Handle, index uint32, valueName *uint16, valueNameSize *uint32, valueType *uint32, data *byte, bufferSize *uint32) (regerrno error) { + r0, _, _ := syscall.Syscall9(procOREnumValue.Addr(), 7, uintptr(key), uintptr(index), uintptr(unsafe.Pointer(valueName)), uintptr(unsafe.Pointer(valueNameSize)), uintptr(unsafe.Pointer(valueType)), uintptr(unsafe.Pointer(data)), uintptr(unsafe.Pointer(bufferSize)), 0, 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func OROpenHive(file *uint16, key *syscall.Handle) (regerrno error) { + r0, _, _ := syscall.Syscall(procOROpenHive.Addr(), 2, uintptr(unsafe.Pointer(file)), uintptr(unsafe.Pointer(key)), 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func OROpenKey(rootKey syscall.Handle, lpSubKeyName *uint16, key *syscall.Handle) (regerrno error) { + r0, _, _ := syscall.Syscall(procOROpenKey.Addr(), 3, uintptr(rootKey), uintptr(unsafe.Pointer(lpSubKeyName)), uintptr(unsafe.Pointer(key))) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func ORQueryInfoKey(key syscall.Handle, lpClass *uint16, lpcClass *uint32, lpcSubKeys *uint32, lpcMaxSubKeyLen *uint32, lpcMaxClassLen *uint32, lpcValues *uint32, lpcMaxValueNameLen *uint32, lpcMaxValueLen *uint32, lpcbSecurityDescriptor *uint32, lpftLastWriteTime uintptr) (regerrno error) { + r0, _, _ := syscall.Syscall12(procORQueryInfoKey.Addr(), 11, uintptr(key), uintptr(unsafe.Pointer(lpClass)), uintptr(unsafe.Pointer(lpcClass)), uintptr(unsafe.Pointer(lpcSubKeys)), uintptr(unsafe.Pointer(lpcMaxSubKeyLen)), uintptr(unsafe.Pointer(lpcMaxClassLen)), uintptr(unsafe.Pointer(lpcValues)), uintptr(unsafe.Pointer(lpcMaxValueNameLen)), uintptr(unsafe.Pointer(lpcMaxValueLen)), uintptr(unsafe.Pointer(lpcbSecurityDescriptor)), uintptr(lpftLastWriteTime), 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} diff --git a/solver/llbsolver/file/backend.go b/solver/llbsolver/file/backend.go index 974c2e04e877..bc02d0d10f57 100644 --- a/solver/llbsolver/file/backend.go +++ b/solver/llbsolver/file/backend.go @@ -5,6 +5,7 @@ import ( "log" "os" "path/filepath" + "runtime" "strings" "time" @@ -25,48 +26,13 @@ func timestampToTime(ts int64) *time.Time { return &tm } -func mapUserToChowner(user *copy.User, idmap *idtools.IdentityMapping) (copy.Chowner, error) { - if user == nil { - return func(old *copy.User) (*copy.User, error) { - if old == nil { - if idmap == nil { - return nil, nil - } - old = ©.User{} // root - // non-nil old is already mapped - if idmap != nil { - identity, err := idmap.ToHost(idtools.Identity{ - UID: old.UID, - GID: old.GID, - }) - if err != nil { - return nil, err - } - return ©.User{UID: identity.UID, GID: identity.GID}, nil - } - } - return old, nil - }, nil - } - u := *user - if idmap != nil { - identity, err := idmap.ToHost(idtools.Identity{ - UID: user.UID, - GID: user.GID, - }) - if err != nil { - return nil, err - } - u.UID = identity.UID - u.GID = identity.GID +func mkdir(ctx context.Context, d string, action pb.FileActionMkDir, user *copy.User, idmap *idtools.IdentityMapping) error { + actionPath := action.Path + if runtime.GOOS == "windows" { + actionPath = strings.Split(actionPath, ":")[1] } - return func(*copy.User) (*copy.User, error) { - return &u, nil - }, nil -} -func mkdir(ctx context.Context, d string, action pb.FileActionMkDir, user *copy.User, idmap *idtools.IdentityMapping) error { - p, err := fs.RootPath(d, filepath.Join("/", action.Path)) + p, err := fs.RootPath(d, filepath.Join("/", actionPath)) if err != nil { return err } diff --git a/solver/llbsolver/file/backend_unix.go b/solver/llbsolver/file/backend_unix.go new file mode 100644 index 000000000000..d01290f300ac --- /dev/null +++ b/solver/llbsolver/file/backend_unix.go @@ -0,0 +1,49 @@ +//go:build !windows +// +build !windows + +package file + +import ( + "github.com/docker/docker/pkg/idtools" + copy "github.com/tonistiigi/fsutil/copy" +) + +func mapUserToChowner(user *copy.User, idmap *idtools.IdentityMapping) (copy.Chowner, error) { + if user == nil { + return func(old *copy.User) (*copy.User, error) { + if old == nil { + if idmap == nil { + return nil, nil + } + old = ©.User{} // root + // non-nil old is already mapped + if idmap != nil { + identity, err := idmap.ToHost(idtools.Identity{ + UID: old.UID, + GID: old.GID, + }) + if err != nil { + return nil, err + } + return ©.User{UID: identity.UID, GID: identity.GID}, nil + } + } + return old, nil + }, nil + } + u := *user + if idmap != nil { + identity, err := idmap.ToHost(idtools.Identity{ + UID: user.UID, + GID: user.GID, + }) + if err != nil { + return nil, err + } + u.UID = identity.UID + u.GID = identity.GID + } + return func(*copy.User) (*copy.User, error) { + return &u, nil + }, nil +} diff --git a/solver/llbsolver/file/backend_windows.go b/solver/llbsolver/file/backend_windows.go new file mode 100644 index 000000000000..e7b96aed338d --- /dev/null +++ b/solver/llbsolver/file/backend_windows.go @@ -0,0 +1,19 @@ +package file + +import ( + "github.com/docker/docker/pkg/idtools" + copy "github.com/tonistiigi/fsutil/copy" +) + +func mapUserToChowner(user *copy.User, idmap *idtools.IdentityMapping) (copy.Chowner, error) { + var copyUser copy.User + if user == nil || user.SID == "" { + copyUser.SID = idtools.ContainerAdministratorSidString + } else { + copyUser.SID = user.SID + } + + return func(*copy.User) (*copy.User, error) { + return ©User, nil + }, nil +} diff --git a/solver/llbsolver/file/user_nolinux.go b/solver/llbsolver/file/user_nolinux.go index 80652fd4abe4..111ec4c70f0a 100644 --- a/solver/llbsolver/file/user_nolinux.go +++ b/solver/llbsolver/file/user_nolinux.go @@ -1,5 +1,5 @@ -//go:build !linux -// +build !linux +//go:build !windows && !linux +// +build !windows,!linux package file diff --git a/solver/llbsolver/file/user_windows.go b/solver/llbsolver/file/user_windows.go new file mode 100644 index 000000000000..f6bb3383fe9e --- /dev/null +++ b/solver/llbsolver/file/user_windows.go @@ -0,0 +1,65 @@ +package file + +import ( + "fmt" + "path/filepath" + + "github.com/docker/docker/pkg/idtools" + "github.com/moby/buildkit/internal/winapi" + "github.com/moby/buildkit/snapshot" + "github.com/moby/buildkit/solver/llbsolver/ops/fileoptypes" + "github.com/moby/buildkit/solver/pb" + "github.com/pkg/errors" + copy "github.com/tonistiigi/fsutil/copy" +) + +func readUser(chopt *pb.ChownOpt, mu, mg fileoptypes.Mount) (*copy.User, error) { + if chopt == nil { + return nil, nil + } + var us copy.User + if chopt.User != nil { + switch u := chopt.User.User.(type) { + case *pb.UserOpt_ByName: + if u.ByName.Name == "ContainerAdministrator" { + us.SID = idtools.ContainerAdministratorSidString + return &us, nil + } + if u.ByName.Name == "ContainerUser" { + us.SID = idtools.ContainerUserSidString + return &us, nil + } + + if mu == nil { + return nil, errors.Errorf("invalid missing user mount") + } + mmu, ok := mu.(*Mount) + if !ok { + return nil, errors.Errorf("invalid mount type %T", mu) + } + lm := snapshot.LocalMounter(mmu.m) + dir, err := lm.Mount() + if err != nil { + return nil, err + } + defer lm.Unmount() + + passwdPath := filepath.Join(dir, "Windows/system32/config/SAM") + users, err := winapi.GetUserInfoFromOfflineSAMHive(passwdPath) + if err != nil { + return nil, fmt.Errorf("parsing %s: %w", passwdPath, err) + } + + for _, usr := range users { + if usr.Username == u.ByName.Name { + us.SID = usr.SIDString + break + } + } + case *pb.UserOpt_ByID: + return nil, fmt.Errorf("UserOpt_ByID not supported on Windows") + } + } + + return &us, nil +}