ash: make evaltree save/restore int suppression depth.
[oweals/busybox.git] / libbb / update_passwd.c
1 /* vi: set sw=4 ts=4: */
2 /*
3  * update_passwd
4  *
5  * update_passwd is a common function for passwd and chpasswd applets;
6  * it is responsible for updating password file (i.e. /etc/passwd or
7  * /etc/shadow) for a given user and password.
8  *
9  * Moved from loginutils/passwd.c by Alexander Shishkin <virtuoso@slind.org>
10  *
11  * Licensed under GPLv2, see file LICENSE in this tarball for details.
12  */
13
14 #include "libbb.h"
15
16 #if ENABLE_SELINUX
17 static void check_selinux_update_passwd(const char *username)
18 {
19         security_context_t context;
20         char *seuser;
21
22         if (getuid() != (uid_t)0 || is_selinux_enabled() == 0)
23                 return;         /* No need to check */
24
25         if (getprevcon_raw(&context) < 0)
26                 bb_perror_msg_and_die("getprevcon failed");
27         seuser = strtok(context, ":");
28         if (!seuser)
29                 bb_error_msg_and_die("invalid context '%s'", context);
30         if (strcmp(seuser, username) != 0) {
31                 if (checkPasswdAccess(PASSWD__PASSWD) != 0)
32                         bb_error_msg_and_die("SELinux: access denied");
33         }
34         if (ENABLE_FEATURE_CLEAN_UP)
35                 freecon(context);
36 }
37 #else
38 #define check_selinux_update_passwd(username) ((void)0)
39 #endif
40
41 int FAST_FUNC update_passwd(const char *filename, const char *username,
42                         const char *new_pw)
43 {
44         struct stat sb;
45         struct flock lock;
46         FILE *old_fp;
47         FILE *new_fp;
48         char *fnamesfx;
49         char *sfx_char;
50         unsigned user_len;
51         int old_fd;
52         int new_fd;
53         int i;
54         int cnt = 0;
55         int ret = -1; /* failure */
56
57         filename = xmalloc_follow_symlinks(filename);
58         if (filename == NULL)
59                 return -1;
60
61         check_selinux_update_passwd(username);
62
63         /* New passwd file, "/etc/passwd+" for now */
64         fnamesfx = xasprintf("%s+", filename);
65         sfx_char = &fnamesfx[strlen(fnamesfx)-1];
66         username = xasprintf("%s:", username);
67         user_len = strlen(username);
68
69         old_fp = fopen(filename, "r+");
70         if (!old_fp)
71                 goto free_mem;
72         old_fd = fileno(old_fp);
73
74         selinux_preserve_fcontext(old_fd);
75
76         /* Try to create "/etc/passwd+". Wait if it exists. */
77         i = 30;
78         do {
79                 // FIXME: on last iteration try w/o O_EXCL but with O_TRUNC?
80                 new_fd = open(fnamesfx, O_WRONLY|O_CREAT|O_EXCL, 0600);
81                 if (new_fd >= 0) goto created;
82                 if (errno != EEXIST) break;
83                 usleep(100000); /* 0.1 sec */
84         } while (--i);
85         bb_perror_msg("cannot create '%s'", fnamesfx);
86         goto close_old_fp;
87
88  created:
89         if (!fstat(old_fd, &sb)) {
90                 fchmod(new_fd, sb.st_mode & 0777); /* ignore errors */
91                 fchown(new_fd, sb.st_uid, sb.st_gid);
92         }
93         new_fp = fdopen(new_fd, "w");
94         if (!new_fp) {
95                 close(new_fd);
96                 goto unlink_new;
97         }
98
99         /* Backup file is "/etc/passwd-" */
100         *sfx_char = '-';
101         /* Delete old backup */
102         i = (unlink(fnamesfx) && errno != ENOENT);
103         /* Create backup as a hardlink to current */
104         if (i || link(filename, fnamesfx))
105                 bb_perror_msg("warning: cannot create backup copy '%s'", fnamesfx);
106         *sfx_char = '+';
107
108         /* Lock the password file before updating */
109         lock.l_type = F_WRLCK;
110         lock.l_whence = SEEK_SET;
111         lock.l_start = 0;
112         lock.l_len = 0;
113         if (fcntl(old_fd, F_SETLK, &lock) < 0)
114                 bb_perror_msg("warning: cannot lock '%s'", filename);
115         lock.l_type = F_UNLCK;
116
117         /* Read current password file, write updated /etc/passwd+ */
118         while (1) {
119                 char *line = xmalloc_fgets(old_fp);
120                 if (!line) break; /* EOF/error */
121                 if (strncmp(username, line, user_len) == 0) {
122                         /* we have a match with "username:"... */
123                         const char *cp = line + user_len;
124                         /* now cp -> old passwd, skip it: */
125                         cp = strchrnul(cp, ':');
126                         /* now cp -> ':' after old passwd or -> "" */
127                         fprintf(new_fp, "%s%s%s", username, new_pw, cp);
128                         cnt++;
129                 } else
130                         fputs(line, new_fp);
131                 free(line);
132         }
133         fcntl(old_fd, F_SETLK, &lock);
134
135         /* We do want all of them to execute, thus | instead of || */
136         if ((ferror(old_fp) | fflush(new_fp) | fsync(new_fd) | fclose(new_fp))
137          || rename(fnamesfx, filename)
138         ) {
139                 /* At least one of those failed */
140                 goto unlink_new;
141         }
142         ret = cnt; /* whee, success! */
143
144  unlink_new:
145         if (ret < 0) unlink(fnamesfx);
146
147  close_old_fp:
148         fclose(old_fp);
149
150  free_mem:
151         free(fnamesfx);
152         free((char *)filename);
153         free((char *)username);
154         return ret;
155 }