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  * Modified to be able to add or delete users, groups and users to/from groups
12  * by Tito Ragusa <farmatito@tiscali.it>
13  *
14  * Licensed under GPLv2, see file LICENSE in this source tree.
15  */
16 #include "libbb.h"
17 
18 #if ENABLE_SELINUX
check_selinux_update_passwd(const char * username)19 static void check_selinux_update_passwd(const char *username)
20 {
21 	security_context_t context;
22 	char *seuser;
23 
24 	if (getuid() != (uid_t)0 || is_selinux_enabled() == 0)
25 		return;  /* No need to check */
26 
27 	if (getprevcon_raw(&context) < 0)
28 		bb_perror_msg_and_die("getprevcon failed");
29 	seuser = strtok(context, ":");
30 	if (!seuser)
31 		bb_error_msg_and_die("invalid context '%s'", context);
32 	if (strcmp(seuser, username) != 0) {
33 		security_class_t tclass;
34 		access_vector_t av;
35 
36 		tclass = string_to_security_class("passwd");
37 		if (tclass == 0)
38 			goto die;
39 		av = string_to_av_perm(tclass, "passwd");
40 		if (av == 0)
41 			goto die;
42 
43 		if (selinux_check_passwd_access(av) != 0)
44  die:
45 			bb_error_msg_and_die("SELinux: access denied");
46 	}
47 	if (ENABLE_FEATURE_CLEAN_UP)
48 		freecon(context);
49 }
50 #else
51 # define check_selinux_update_passwd(username) ((void)0)
52 #endif
53 
54 /*
55  1) add a user: update_passwd(FILE, USER, REMAINING_PWLINE, NULL)
56     only if CONFIG_ADDUSER=y and applet_name[0] == 'a' like in adduser
57 
58  2) add a group: update_passwd(FILE, GROUP, REMAINING_GRLINE, NULL)
59     only if CONFIG_ADDGROUP=y and applet_name[0] == 'a' like in addgroup
60 
61  3) add a user to a group: update_passwd(FILE, GROUP, NULL, MEMBER)
62     only if CONFIG_FEATURE_ADDUSER_TO_GROUP=y, applet_name[0] == 'a'
63     like in addgroup and member != NULL
64 
65  4) delete a user: update_passwd(FILE, USER, NULL, NULL)
66 
67  5) delete a group: update_passwd(FILE, GROUP, NULL, NULL)
68 
69  6) delete a user from a group: update_passwd(FILE, GROUP, NULL, MEMBER)
70     only if CONFIG_FEATURE_DEL_USER_FROM_GROUP=y and member != NULL
71 
72  7) change user's password: update_passwd(FILE, USER, NEW_PASSWD, NULL)
73     only if CONFIG_PASSWD=y and applet_name[0] == 'p' like in passwd
74     or if CONFIG_CHPASSWD=y and applet_name[0] == 'c' like in chpasswd
75 
76  8) delete a user from all groups: update_passwd(FILE, NULL, NULL, MEMBER)
77 
78  This function does not validate the arguments fed to it
79  so the calling program should take care of that.
80 
81  Returns number of lines changed, or -1 on error.
82 */
update_passwd(const char * filename,const char * name,const char * new_passwd,const char * member)83 int FAST_FUNC update_passwd(const char *filename,
84 		const char *name,
85 		const char *new_passwd,
86 		const char *member)
87 {
88 #if !(ENABLE_FEATURE_ADDUSER_TO_GROUP || ENABLE_FEATURE_DEL_USER_FROM_GROUP)
89 #define member NULL
90 #endif
91 	struct stat sb;
92 	struct flock lock;
93 	FILE *old_fp;
94 	FILE *new_fp;
95 	char *fnamesfx;
96 	char *sfx_char;
97 	char *name_colon;
98 	int old_fd;
99 	int new_fd;
100 	int i;
101 	int changed_lines;
102 	int ret = -1; /* failure */
103 	/* used as a bool: "are we modifying /etc/shadow?" */
104 #if ENABLE_FEATURE_SHADOWPASSWDS
105 	const char *shadow = strstr(filename, "shadow");
106 #else
107 # define shadow NULL
108 #endif
109 
110 	filename = xmalloc_follow_symlinks(filename);
111 	if (filename == NULL)
112 		return ret;
113 
114 	if (name)
115 		check_selinux_update_passwd(name);
116 
117 	/* New passwd file, "/etc/passwd+" for now */
118 	fnamesfx = xasprintf("%s+", filename);
119 	sfx_char = &fnamesfx[strlen(fnamesfx)-1];
120 	name_colon = xasprintf("%s:", name ? name : "");
121 
122 	if (shadow)
123 		old_fp = fopen(filename, "r+");
124 	else
125 		old_fp = fopen_or_warn(filename, "r+");
126 	if (!old_fp) {
127 		if (shadow)
128 			ret = 0; /* missing shadow is not an error */
129 		goto free_mem;
130 	}
131 	old_fd = fileno(old_fp);
132 
133 	selinux_preserve_fcontext(old_fd);
134 
135 	/* Try to create "/etc/passwd+". Wait if it exists. */
136 	i = 30;
137 	do {
138 		// FIXME: on last iteration try w/o O_EXCL but with O_TRUNC?
139 		new_fd = open(fnamesfx, O_WRONLY|O_CREAT|O_EXCL, 0600);
140 		if (new_fd >= 0) goto created;
141 		if (errno != EEXIST) break;
142 		usleep(100000); /* 0.1 sec */
143 	} while (--i);
144 	bb_perror_msg("can't create '%s'", fnamesfx);
145 	goto close_old_fp;
146 
147  created:
148 	if (fstat(old_fd, &sb) == 0) {
149 		fchmod(new_fd, sb.st_mode & 0777); /* ignore errors */
150 		fchown(new_fd, sb.st_uid, sb.st_gid);
151 	}
152 	errno = 0;
153 	new_fp = xfdopen_for_write(new_fd);
154 
155 	/* Backup file is "/etc/passwd-" */
156 	*sfx_char = '-';
157 	/* Delete old backup */
158 	i = (unlink(fnamesfx) && errno != ENOENT);
159 	/* Create backup as a hardlink to current */
160 	if (i || link(filename, fnamesfx))
161 		bb_perror_msg("warning: can't create backup copy '%s'",
162 				fnamesfx);
163 	*sfx_char = '+';
164 
165 	/* Lock the password file before updating */
166 	lock.l_type = F_WRLCK;
167 	lock.l_whence = SEEK_SET;
168 	lock.l_start = 0;
169 	lock.l_len = 0;
170 	if (fcntl(old_fd, F_SETLK, &lock) < 0)
171 		bb_perror_msg("warning: can't lock '%s'", filename);
172 	lock.l_type = F_UNLCK;
173 
174 	/* Read current password file, write updated /etc/passwd+ */
175 	changed_lines = 0;
176 	while (1) {
177 		char *cp, *line;
178 
179 		line = xmalloc_fgetline(old_fp);
180 		if (!line) /* EOF/error */
181 			break;
182 
183 #if ENABLE_FEATURE_ADDUSER_TO_GROUP || ENABLE_FEATURE_DEL_USER_FROM_GROUP
184 		if (!name && member) {
185 			/* Delete member from all groups */
186 			/* line is "GROUP:PASSWD:[member1[,member2]...]" */
187 			unsigned member_len = strlen(member);
188 			char *list = strrchr(line, ':');
189 			while (list) {
190 				list++;
191  next_list_element:
192 				if (is_prefixed_with(list, member)) {
193 					char c;
194 					changed_lines++;
195 					c = list[member_len];
196 					if (c == '\0') {
197 						if (list[-1] == ',')
198 							list--;
199 						*list = '\0';
200 						break;
201 					}
202 					if (c == ',') {
203 						overlapping_strcpy(list, list + member_len + 1);
204 						goto next_list_element;
205 					}
206 					changed_lines--;
207 				}
208 				list = strchr(list, ',');
209 			}
210 			fprintf(new_fp, "%s\n", line);
211 			goto next;
212 		}
213 #endif
214 
215 		cp = is_prefixed_with(line, name_colon);
216 		if (!cp) {
217 			fprintf(new_fp, "%s\n", line);
218 			goto next;
219 		}
220 
221 		/* We have a match with "name:"... */
222 		/* cp points past "name:" */
223 
224 #if ENABLE_FEATURE_ADDUSER_TO_GROUP || ENABLE_FEATURE_DEL_USER_FROM_GROUP
225 		if (member) {
226 			/* It's actually /etc/group+, not /etc/passwd+ */
227 			if (ENABLE_FEATURE_ADDUSER_TO_GROUP
228 			 && applet_name[0] == 'a'
229 			) {
230 				/* Add user to group */
231 				fprintf(new_fp, "%s%s%s\n", line,
232 					last_char_is(line, ':') ? "" : ",",
233 					member);
234 				changed_lines++;
235 			} else if (ENABLE_FEATURE_DEL_USER_FROM_GROUP
236 			/* && applet_name[0] == 'd' */
237 			) {
238 				/* Delete user from group */
239 				char *tmp;
240 				const char *fmt = "%s";
241 
242 				/* find the start of the member list: last ':' */
243 				cp = strrchr(line, ':');
244 				/* cut it */
245 				*cp++ = '\0';
246 				/* write the cut line name:passwd:gid:
247 				 * or name:!:: */
248 				fprintf(new_fp, "%s:", line);
249 				/* parse the tokens of the member list */
250 				tmp = cp;
251 				while ((cp = strsep(&tmp, ",")) != NULL) {
252 					if (strcmp(member, cp) != 0) {
253 						fprintf(new_fp, fmt, cp);
254 						fmt = ",%s";
255 					} else {
256 						/* found member, skip it */
257 						changed_lines++;
258 					}
259 				}
260 				fprintf(new_fp, "\n");
261 			}
262 		} else
263 #endif
264 		if ((ENABLE_PASSWD && applet_name[0] == 'p')
265 		 || (ENABLE_CHPASSWD && applet_name[0] == 'c')
266 		) {
267 			/* Change passwd */
268 			cp = strchrnul(cp, ':'); /* move past old passwd */
269 
270 			if (shadow && *cp == ':') {
271 				/* /etc/shadow's field 3 (passwd change date) needs updating */
272 				/* move past old change date */
273 				cp = strchrnul(cp + 1, ':');
274 				/* "name:" + "new_passwd" + ":" + "change date" + ":rest of line" */
275 				fprintf(new_fp, "%s%s:%u%s\n", name_colon, new_passwd,
276 					(unsigned)(time(NULL)) / (24*60*60), cp);
277 			} else {
278 				/* "name:" + "new_passwd" + ":rest of line" */
279 				fprintf(new_fp, "%s%s%s\n", name_colon, new_passwd, cp);
280 			}
281 			changed_lines++;
282 		} /* else delete user or group: skip the line */
283  next:
284 		free(line);
285 	}
286 
287 	if (changed_lines == 0) {
288 #if ENABLE_FEATURE_ADDUSER_TO_GROUP || ENABLE_FEATURE_DEL_USER_FROM_GROUP
289 		if (member) {
290 			if (ENABLE_ADDGROUP && applet_name[0] == 'a')
291 				bb_error_msg("can't find %s in %s", name, filename);
292 			if (ENABLE_DELGROUP && applet_name[0] == 'd')
293 				bb_error_msg("can't find %s in %s", member, filename);
294 		}
295 #endif
296 		if ((ENABLE_ADDUSER || ENABLE_ADDGROUP)
297 		 && applet_name[0] == 'a' && !member
298 		) {
299 			/* add user or group */
300 			fprintf(new_fp, "%s%s\n", name_colon, new_passwd);
301 			changed_lines++;
302 		}
303 	}
304 
305 	fcntl(old_fd, F_SETLK, &lock);
306 
307 	/* We do want all of them to execute, thus | instead of || */
308 	errno = 0;
309 	if ((ferror(old_fp) | fflush(new_fp) | fsync(new_fd) | fclose(new_fp))
310 	 || rename(fnamesfx, filename)
311 	) {
312 		/* At least one of those failed */
313 		bb_perror_nomsg();
314 		goto unlink_new;
315 	}
316 	/* Success: ret >= 0 */
317 	ret = changed_lines;
318 
319  unlink_new:
320 	if (ret < 0)
321 		unlink(fnamesfx);
322 
323  close_old_fp:
324 	fclose(old_fp);
325 
326  free_mem:
327 	free(fnamesfx);
328 	free((char *)filename);
329 	free(name_colon);
330 	return ret;
331 }
332