k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/kubeadm/app/util/users/users_linux.go (about) 1 //go:build linux 2 // +build linux 3 4 /* 5 Copyright 2021 The Kubernetes Authors. 6 7 Licensed under the Apache License, Version 2.0 (the "License"); 8 you may not use this file except in compliance with the License. 9 You may obtain a copy of the License at 10 11 http://www.apache.org/licenses/LICENSE-2.0 12 13 Unless required by applicable law or agreed to in writing, software 14 distributed under the License is distributed on an "AS IS" BASIS, 15 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 See the License for the specific language governing permissions and 17 limitations under the License. 18 */ 19 20 package users 21 22 import ( 23 "bytes" 24 "fmt" 25 "io" 26 "os" 27 "path/filepath" 28 "sort" 29 "strconv" 30 "strings" 31 "syscall" 32 "time" 33 34 "github.com/pkg/errors" 35 36 "k8s.io/klog/v2" 37 38 "k8s.io/kubernetes/cmd/kubeadm/app/constants" 39 ) 40 41 // EntryMap holds a map of user or group entries. 42 type EntryMap struct { 43 entries map[string]*entry 44 } 45 46 // UsersAndGroups is a structure that holds entry maps of users and groups. 47 // It is returned by AddUsersAndGroups. 48 type UsersAndGroups struct { 49 // Users is an entry map of users. 50 Users *EntryMap 51 // Groups is an entry map of groups. 52 Groups *EntryMap 53 } 54 55 // entry is a structure that holds information about a UNIX user or group. 56 // It partialially conforms parsing of both users from /etc/passwd and groups from /etc/group. 57 type entry struct { 58 name string 59 id int64 60 gid int64 61 userNames []string 62 shell string 63 } 64 65 // limits is used to hold information about the minimum and maximum system ranges for UID and GID. 66 type limits struct { 67 minUID, maxUID, minGID, maxGID int64 68 } 69 70 const ( 71 // These are constants used when parsing /etc/passwd or /etc/group in terms of how many 72 // fields and entry has. 73 totalFieldsGroup = 4 74 totalFieldsUser = 7 75 76 // klogLevel holds the klog level to use for output. 77 klogLevel = 5 78 79 // noshell holds a path to a binary to disable shell login. 80 noshell = "/bin/false" 81 82 // These are constants for the default system paths on Linux. 83 fileEtcLoginDefs = "/etc/login.defs" 84 fileEtcPasswd = "/etc/passwd" 85 fileEtcGroup = "/etc/group" 86 ) 87 88 var ( 89 // these entries hold the users and groups to create as defined in: 90 // https://git.k8s.io/enhancements/keps/sig-cluster-lifecycle/kubeadm/2568-kubeadm-non-root-control-plane 91 usersToCreateSpec = []*entry{ 92 {name: constants.EtcdUserName}, 93 {name: constants.KubeAPIServerUserName}, 94 {name: constants.KubeControllerManagerUserName}, 95 {name: constants.KubeSchedulerUserName}, 96 } 97 groupsToCreateSpec = []*entry{ 98 {name: constants.EtcdUserName, userNames: []string{constants.EtcdUserName}}, 99 {name: constants.KubeAPIServerUserName, userNames: []string{constants.KubeAPIServerUserName}}, 100 {name: constants.KubeControllerManagerUserName, userNames: []string{constants.KubeControllerManagerUserName}}, 101 {name: constants.KubeSchedulerUserName, userNames: []string{constants.KubeSchedulerUserName}}, 102 {name: constants.ServiceAccountKeyReadersGroupName, userNames: []string{constants.KubeAPIServerUserName, constants.KubeControllerManagerUserName}}, 103 } 104 105 // defaultLimits holds the default limits in case values are missing in /etc/login.defs 106 defaultLimits = &limits{minUID: 100, maxUID: 999, minGID: 100, maxGID: 999} 107 ) 108 109 // ID returns the ID for an entry based on the entry name. 110 // In case of a user entry it returns the user UID. 111 // In case of a group entry it returns the group GID. 112 // It returns nil if no such entry exists. 113 func (u *EntryMap) ID(name string) *int64 { 114 entry, ok := u.entries[name] 115 if !ok { 116 return nil 117 } 118 id := entry.id 119 return &id 120 } 121 122 // String converts an EntryMap object to a readable string. 123 func (u *EntryMap) String() string { 124 lines := make([]string, 0, len(u.entries)) 125 for k, e := range u.entries { 126 lines = append(lines, fmt.Sprintf("%s{%d,%d};", k, e.id, e.gid)) 127 } 128 sort.Strings(lines) 129 return strings.Join(lines, "") 130 } 131 132 // AddUsersAndGroups is a public wrapper around addUsersAndGroupsImpl with default system file paths. 133 func AddUsersAndGroups() (*UsersAndGroups, error) { 134 return addUsersAndGroupsImpl(fileEtcLoginDefs, fileEtcPasswd, fileEtcGroup) 135 } 136 137 // addUsersAndGroupsImpl adds the managed users and groups to the files specified 138 // by pathUsers and pathGroups. It uses the file specified with pathLoginDef to 139 // determine limits for UID and GID. If managed users and groups exist in these files 140 // validation is performed on them. The function returns a pointer to a Users object 141 // that can be used to return UID and GID of managed users. 142 func addUsersAndGroupsImpl(pathLoginDef, pathUsers, pathGroups string) (*UsersAndGroups, error) { 143 klog.V(1).Info("Adding managed users and groups") 144 klog.V(klogLevel).Infof("Parsing %q", pathLoginDef) 145 146 // Read and parse /etc/login.def. Some distributions might be missing this file, which makes 147 // them non-standard. If an error occurs fallback to defaults by passing an empty string 148 // to parseLoginDefs(). 149 var loginDef string 150 f, close, err := openFileWithLock(pathLoginDef) 151 if err != nil { 152 klog.V(1).Infof("Could not open %q, using default system limits: %v", pathLoginDef, err) 153 } else { 154 loginDef, err = readFile(f) 155 if err != nil { 156 klog.V(1).Infof("Could not read %q, using default system limits: %v", pathLoginDef, err) 157 } 158 close() 159 } 160 limits, err := parseLoginDefs(loginDef) 161 if err != nil { 162 return nil, err 163 } 164 165 klog.V(klogLevel).Infof("Using system UID/GID limits: %+v", limits) 166 klog.V(klogLevel).Infof("Parsing %q and %q", pathUsers, pathGroups) 167 168 // Open /etc/passwd and /etc/group with locks. 169 fUsers, close, err := openFileWithLock(pathUsers) 170 if err != nil { 171 return nil, err 172 } 173 defer close() 174 fGroups, close, err := openFileWithLock(pathGroups) 175 if err != nil { 176 return nil, err 177 } 178 defer close() 179 180 // Read the files. 181 fileUsers, err := readFile(fUsers) 182 if err != nil { 183 return nil, err 184 } 185 fileGroups, err := readFile(fGroups) 186 if err != nil { 187 return nil, err 188 } 189 190 // Parse the files. 191 users, err := parseEntries(fileUsers, totalFieldsUser) 192 if err != nil { 193 return nil, errors.Wrapf(err, "could not parse %q", pathUsers) 194 } 195 groups, err := parseEntries(fileGroups, totalFieldsGroup) 196 if err != nil { 197 return nil, errors.Wrapf(err, "could not parse %q", pathGroups) 198 } 199 200 klog.V(klogLevel).Info("Validating existing users and groups") 201 202 // Validate for existing tracked entries based on limits. 203 usersToCreate, groupsToCreate, err := validateEntries(users, groups, limits) 204 if err != nil { 205 return nil, errors.Wrap(err, "error validating existing users and groups") 206 } 207 208 // Allocate and assign IDs to users / groups. 209 allocUIDs, err := allocateIDs(users, limits.minUID, limits.maxUID, len(usersToCreate)) 210 if err != nil { 211 return nil, err 212 } 213 allocGIDs, err := allocateIDs(groups, limits.minGID, limits.maxGID, len(groupsToCreate)) 214 if err != nil { 215 return nil, err 216 } 217 if err := assignUserAndGroupIDs(groups, usersToCreate, groupsToCreate, allocUIDs, allocGIDs); err != nil { 218 return nil, err 219 } 220 221 if len(usersToCreate) > 0 { 222 klog.V(klogLevel).Infof("Adding users: %s", entriesToString(usersToCreate)) 223 } 224 if len(groupsToCreate) > 0 { 225 klog.V(klogLevel).Infof("Adding groups: %s", entriesToString(groupsToCreate)) 226 } 227 228 // Add users and groups. 229 fileUsers = addEntries(fileUsers, usersToCreate, createUser) 230 fileGroups = addEntries(fileGroups, groupsToCreate, createGroup) 231 232 // Write the files. 233 klog.V(klogLevel).Infof("Writing %q and %q", pathUsers, pathGroups) 234 if err := writeFile(fUsers, fileUsers); err != nil { 235 return nil, err 236 } 237 if err := writeFile(fGroups, fileGroups); err != nil { 238 return nil, err 239 } 240 241 // Prepare the maps of users and groups. 242 usersConcat := append(users, usersToCreate...) 243 mapUsers, err := entriesToEntryMap(usersConcat, usersToCreateSpec) 244 if err != nil { 245 return nil, err 246 } 247 groupsConcat := append(groups, groupsToCreate...) 248 mapGroups, err := entriesToEntryMap(groupsConcat, groupsToCreateSpec) 249 if err != nil { 250 return nil, err 251 } 252 return &UsersAndGroups{Users: mapUsers, Groups: mapGroups}, nil 253 } 254 255 // RemoveUsersAndGroups is a public wrapper around removeUsersAndGroupsImpl with 256 // default system file paths. 257 func RemoveUsersAndGroups() error { 258 return removeUsersAndGroupsImpl(fileEtcPasswd, fileEtcGroup) 259 } 260 261 // removeUsersAndGroupsImpl removes the managed users and groups from the files specified 262 // by pathUsers and pathGroups. 263 func removeUsersAndGroupsImpl(pathUsers, pathGroups string) error { 264 klog.V(1).Info("Removing managed users and groups") 265 klog.V(klogLevel).Infof("Opening %q and %q", pathUsers, pathGroups) 266 267 // Open /etc/passwd and /etc/group. 268 fUsers, close, err := openFileWithLock(pathUsers) 269 if err != nil { 270 return err 271 } 272 defer close() 273 fGroups, close, err := openFileWithLock(pathGroups) 274 if err != nil { 275 return err 276 } 277 defer close() 278 279 // Read the files. 280 fileUsers, err := readFile(fUsers) 281 if err != nil { 282 return err 283 } 284 fileGroups, err := readFile(fGroups) 285 if err != nil { 286 return err 287 } 288 289 klog.V(klogLevel).Infof("Removing users: %s", entriesToString(usersToCreateSpec)) 290 klog.V(klogLevel).Infof("Removing groups: %s", entriesToString(groupsToCreateSpec)) 291 292 // Delete users / groups. 293 fileUsers, _ = removeEntries(fileUsers, usersToCreateSpec) 294 fileGroups, _ = removeEntries(fileGroups, groupsToCreateSpec) 295 296 klog.V(klogLevel).Infof("Writing %q and %q", pathUsers, pathGroups) 297 298 // Write the files. 299 if err := writeFile(fUsers, fileUsers); err != nil { 300 return err 301 } 302 if err := writeFile(fGroups, fileGroups); err != nil { 303 return err 304 } 305 306 return nil 307 } 308 309 // parseLoginDefs can be used to parse an /etc/login.defs file and obtain system ranges for UID and GID. 310 // Passing an empty string will return the defaults. The defaults are 100-999 for both UID and GID. 311 func parseLoginDefs(file string) (*limits, error) { 312 l := *defaultLimits 313 if len(file) == 0 { 314 return &l, nil 315 } 316 var mapping = map[string]*int64{ 317 "SYS_UID_MIN": &l.minUID, 318 "SYS_UID_MAX": &l.maxUID, 319 "SYS_GID_MIN": &l.minGID, 320 "SYS_GID_MAX": &l.maxGID, 321 } 322 lines := strings.Split(file, "\n") 323 for i, line := range lines { 324 for k, v := range mapping { 325 // A line must start with one of the definitions 326 if !strings.HasPrefix(line, k) { 327 continue 328 } 329 line = strings.TrimPrefix(line, k) 330 line = strings.TrimSpace(line) 331 val, err := strconv.ParseInt(line, 10, 64) 332 if err != nil { 333 return nil, errors.Wrapf(err, "could not parse value for %s at line %d", k, i) 334 } 335 *v = val 336 } 337 } 338 return &l, nil 339 } 340 341 // parseEntries can be used to parse an /etc/passwd or /etc/group file as their format is similar. 342 // It returns a slice of entries obtained from the file. 343 // https://www.cyberciti.biz/faq/understanding-etcpasswd-file-format/ 344 // https://www.cyberciti.biz/faq/understanding-etcgroup-file/ 345 func parseEntries(file string, totalFields int) ([]*entry, error) { 346 if totalFields != totalFieldsUser && totalFields != totalFieldsGroup { 347 return nil, errors.Errorf("unsupported total fields for entry parsing: %d", totalFields) 348 } 349 lines := strings.Split(file, "\n") 350 entries := []*entry{} 351 for i, line := range lines { 352 line = strings.TrimSpace(line) 353 if len(line) == 0 { 354 continue 355 } 356 fields := strings.Split(line, ":") 357 if len(fields) != totalFields { 358 return nil, errors.Errorf("entry must have %d fields separated by ':', "+ 359 "got %d at line %d: %s", totalFields, len(fields), i, line) 360 } 361 id, err := strconv.ParseInt(fields[2], 10, 64) 362 if err != nil { 363 return nil, errors.Wrapf(err, "error parsing id at line %d", i) 364 } 365 entry := &entry{name: fields[0], id: id} 366 if totalFields == totalFieldsGroup { 367 entry.userNames = strings.Split(fields[3], ",") 368 } else { 369 gid, err := strconv.ParseInt(fields[3], 10, 64) 370 if err != nil { 371 return nil, errors.Wrapf(err, "error parsing GID at line %d", i) 372 } 373 entry.gid = gid 374 entry.shell = fields[6] 375 } 376 entries = append(entries, entry) 377 } 378 return entries, nil 379 } 380 381 // validateEntries takes user and group entries and validates if these entries are valid based on limits, 382 // mapping between users and groups and specs. Returns slices of missing user and group entries that must be created. 383 // Returns an error if existing users and groups do not match requirements. 384 func validateEntries(users, groups []*entry, limits *limits) ([]*entry, []*entry, error) { 385 u := []*entry{} 386 g := []*entry{} 387 // Validate users 388 for _, uc := range usersToCreateSpec { 389 for _, user := range users { 390 if uc.name != user.name { 391 continue 392 } 393 // Found existing user 394 if user.id < limits.minUID || user.id > limits.maxUID { 395 return nil, nil, errors.Errorf("UID %d for user %q is outside the system UID range: %d - %d", 396 user.id, user.name, limits.minUID, limits.maxUID) 397 } 398 if user.shell != noshell { 399 return nil, nil, errors.Errorf("user %q has unexpected shell %q; expected %q", 400 user.name, user.shell, noshell) 401 } 402 for _, g := range groups { 403 if g.id != user.gid { 404 continue 405 } 406 // Found matching group GID for user GID 407 if g.name != uc.name { 408 return nil, nil, errors.Errorf("user %q has GID %d but the group with that GID is not named %q", 409 uc.name, g.id, uc.name) 410 } 411 goto skipUser // Valid group GID and name; skip 412 } 413 return nil, nil, errors.Errorf("could not find group with GID %d for user %q", user.gid, user.name) 414 } 415 u = append(u, uc) 416 skipUser: 417 } 418 // validate groups 419 for _, gc := range groupsToCreateSpec { 420 for _, group := range groups { 421 if gc.name != group.name { 422 continue 423 } 424 if group.id < limits.minGID || group.id > limits.maxGID { 425 return nil, nil, errors.Errorf("GID %d for user %q is outside the system UID range: %d - %d", 426 group.id, group.name, limits.minGID, limits.maxGID) 427 } 428 u1 := strings.Join(gc.userNames, ",") 429 u2 := strings.Join(group.userNames, ",") 430 if u1 != u2 { 431 return nil, nil, errors.Errorf("expected users %q for group %q; got %q", 432 u1, gc.name, u2) 433 } 434 goto skipGroup // group has valid users; skip 435 } 436 g = append(g, gc) 437 skipGroup: 438 } 439 return u, g, nil 440 } 441 442 // allocateIDs takes a list of entries and based on minimum and maximum ID allocates a "total" of IDs. 443 func allocateIDs(entries []*entry, min, max int64, total int) ([]int64, error) { 444 if total == 0 { 445 return []int64{}, nil 446 } 447 ids := make([]int64, 0, total) 448 for i := min; i < max+1; i++ { 449 i64 := int64(i) 450 for _, e := range entries { 451 if i64 == e.id { 452 goto continueLoop 453 } 454 } 455 ids = append(ids, i64) 456 if len(ids) == total { 457 return ids, nil 458 } 459 continueLoop: 460 } 461 return nil, errors.Errorf("could not allocate %d IDs based on existing entries in the range: %d - %d", 462 total, min, max) 463 } 464 465 // addEntries takes /etc/passwd or /etc/group file content and appends entries to it based 466 // on a createEntry function. Returns the updated contents of the file. 467 func addEntries(file string, entries []*entry, createEntry func(*entry) string) string { 468 out := file 469 newLines := make([]string, 0, len(entries)) 470 for _, e := range entries { 471 newLines = append(newLines, createEntry(e)) 472 } 473 newLinesStr := "" 474 if len(newLines) > 0 { 475 if !strings.HasSuffix(out, "\n") { // Append a new line if its missing. 476 newLinesStr = "\n" 477 } 478 newLinesStr += strings.Join(newLines, "\n") + "\n" 479 } 480 return out + newLinesStr 481 } 482 483 // removeEntries takes /etc/passwd or /etc/group file content and deletes entries from them 484 // by name matching. Returns the updated contents of the file and the number of entries removed. 485 func removeEntries(file string, entries []*entry) (string, int) { 486 lines := strings.Split(file, "\n") 487 total := len(lines) - len(entries) 488 if total < 0 { 489 total = 0 490 } 491 newLines := make([]string, 0, total) 492 removed := 0 493 for _, line := range lines { 494 for _, entry := range entries { 495 if strings.HasPrefix(line, entry.name+":") { 496 removed++ 497 goto continueLoop 498 } 499 } 500 newLines = append(newLines, line) 501 continueLoop: 502 } 503 return strings.Join(newLines, "\n"), removed 504 } 505 506 // assignUserAndGroupIDs takes the list of existing groups, the users and groups to be created, 507 // and assigns UIDs and GIDs to the users and groups to be created based on a list of provided UIDs and GIDs. 508 // Returns an error if not enough UIDs or GIDs are passed. It does not perform any other validation. 509 func assignUserAndGroupIDs(groups, usersToCreate, groupsToCreate []*entry, uids, gids []int64) error { 510 if len(gids) < len(groupsToCreate) { 511 return errors.Errorf("not enough GIDs to assign to groups: have %d, want %d", len(gids), len(groupsToCreate)) 512 } 513 if len(uids) < len(usersToCreate) { 514 return errors.Errorf("not enough UIDs to assign to users: have %d, want %d", len(uids), len(usersToCreate)) 515 } 516 for i := range groupsToCreate { 517 groupsToCreate[i].id = gids[i] 518 } 519 // Concat the list of old and new groups to find a matching GID. 520 groupsConcat := append([]*entry{}, groups...) 521 groupsConcat = append(groupsConcat, groupsToCreate...) 522 for i := range usersToCreate { 523 usersToCreate[i].id = uids[i] 524 for _, g := range groupsConcat { 525 if usersToCreate[i].name == g.name { 526 usersToCreate[i].gid = g.id 527 break 528 } 529 } 530 } 531 return nil 532 } 533 534 // createGroup is a helper function to produce a group from entry. 535 func createGroup(e *entry) string { 536 return fmt.Sprintf("%s:x:%d:%s", e.name, e.id, strings.Join(e.userNames, ",")) 537 } 538 539 // createUser is a helper function to produce a user from entry. 540 func createUser(e *entry) string { 541 return fmt.Sprintf("%s:x:%d:%d:::/bin/false", e.name, e.id, e.gid) 542 } 543 544 // entriesToEntryMap takes a list of entries and prepares an EntryMap object. 545 func entriesToEntryMap(entries, spec []*entry) (*EntryMap, error) { 546 m := map[string]*entry{} 547 for _, spec := range spec { 548 for _, e := range entries { 549 if spec.name == e.name { 550 entry := *e 551 m[e.name] = &entry 552 goto continueLoop 553 } 554 } 555 return nil, errors.Errorf("could not find entry %q in the list", spec.name) 556 continueLoop: 557 } 558 return &EntryMap{entries: m}, nil 559 } 560 561 // entriesToString is a utility to convert a list of entries to string. 562 func entriesToString(entries []*entry) string { 563 lines := make([]string, 0, len(entries)) 564 for _, e := range entries { 565 lines = append(lines, e.name) 566 } 567 sort.Strings(lines) 568 return strings.Join(lines, ",") 569 } 570 571 // openFileWithLock opens the file at path by acquiring an exclive write lock. 572 // The returned close() function should be called to release the lock and close the file. 573 // If a lock cannot be obtained the function fails after a period of time. 574 func openFileWithLock(path string) (f *os.File, close func(), err error) { 575 f, err = os.OpenFile(path, os.O_RDWR, os.ModePerm) 576 if err != nil { 577 return nil, nil, err 578 } 579 deadline := time.Now().Add(time.Second * 5) 580 for { 581 // If another process is holding a write lock, this call will exit 582 // with an error. F_SETLK is used instead of F_SETLKW to avoid 583 // the case where a runaway process grabs the exclusive lock and 584 // blocks this call indefinitely. 585 // https://man7.org/linux/man-pages/man2/fcntl.2.html 586 lock := syscall.Flock_t{Type: syscall.F_WRLCK} 587 if err = syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, &lock); err == nil { 588 break 589 } 590 time.Sleep(200 * time.Millisecond) 591 if time.Now().After(deadline) { 592 err = errors.Wrapf(err, "timeout attempting to obtain lock on file %q", path) 593 break 594 } 595 } 596 if err != nil { 597 f.Close() 598 return nil, nil, err 599 } 600 close = func() { 601 // This function should be called once operations with the file are finished. 602 // It unlocks the file and closes it. 603 unlock := syscall.Flock_t{Type: syscall.F_UNLCK} 604 syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, &unlock) 605 f.Close() 606 } 607 return f, close, nil 608 } 609 610 // readFile reads a File into a string. 611 func readFile(f *os.File) (string, error) { 612 buf := bytes.NewBuffer(nil) 613 if _, err := f.Seek(0, io.SeekStart); err != nil { 614 return "", err 615 } 616 if _, err := io.Copy(buf, f); err != nil { 617 return "", err 618 } 619 return buf.String(), nil 620 } 621 622 // writeFile writes a string to a File. 623 func writeFile(f *os.File, str string) error { 624 if _, err := f.Seek(0, io.SeekStart); err != nil { 625 return err 626 } 627 if _, err := f.Write([]byte(str)); err != nil { 628 return err 629 } 630 if err := f.Truncate(int64(len(str))); err != nil { 631 return err 632 } 633 return nil 634 } 635 636 // UpdatePathOwnerAndPermissions updates the owner and permissions of the given path. 637 // If the path is a directory it is not recursively updated. 638 func UpdatePathOwnerAndPermissions(path string, uid, gid int64, perms uint32) error { 639 if err := os.Chown(path, int(uid), int(gid)); err != nil { 640 return errors.Wrapf(err, "failed to update owner of %q to uid: %d and gid: %d", path, uid, gid) 641 } 642 fm := os.FileMode(perms) 643 if err := os.Chmod(path, fm); err != nil { 644 return errors.Wrapf(err, "failed to update permissions of %q to %s", path, fm.String()) 645 } 646 return nil 647 } 648 649 // UpdatePathOwner recursively updates the owners of a directory. 650 // It is equivalent to calling `chown -R uid:gid /path/to/dir`. 651 func UpdatePathOwner(dirPath string, uid, gid int64) error { 652 err := filepath.WalkDir(dirPath, func(path string, d os.DirEntry, err error) error { 653 if err := os.Chown(path, int(uid), int(gid)); err != nil { 654 return errors.Wrapf(err, "failed to update owner of %q to uid: %d and gid: %d", path, uid, gid) 655 } 656 return nil 657 }) 658 return err 659 }