summaryrefslogtreecommitdiff
path: root/libbb/update_passwd.c
blob: d10e863c6c63de728e6c818e54993b5a4de3a4c7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/* vi: set sw=4 ts=4: */
/*
 * update_passwd
 *
 * update_passwd is a common function for passwd and chpasswd applets;
 * it is responsible for updating password file (i.e. /etc/passwd or
 * /etc/shadow) for a given user and password.
 *
 * Moved from loginutils/passwd.c by Alexander Shishkin <virtuoso@slind.org>
 */

#include "libbb.h"

#if ENABLE_SELINUX
static void check_selinux_update_passwd(const char *username)
{
	security_context_t context;
	char *seuser;

	if (getuid() != (uid_t)0 || is_selinux_enabled() == 0)
		return;		/* No need to check */

	if (getprevcon_raw(&context) < 0)
		bb_perror_msg_and_die("getprevcon failed");
	seuser = strtok(context, ":");
	if (!seuser)
		bb_error_msg_and_die("invalid context '%s'", context);
	if (strcmp(seuser, username) != 0) {
		if (checkPasswdAccess(PASSWD__PASSWD) != 0)
			bb_error_msg_and_die("SELinux: access denied");
	}
	if (ENABLE_FEATURE_CLEAN_UP)
		freecon(context);
}
#else
#define check_selinux_update_passwd(username) ((void)0)
#endif

int update_passwd(const char *filename, const char *username,
			const char *new_pw)
{
	struct stat sb;
	struct flock lock;
	FILE *old_fp;
	FILE *new_fp;
	char *fnamesfx;
	char *sfx_char;
	unsigned user_len;
	int old_fd;
	int new_fd;
	int i;
	int cnt = 0;
	int ret = -1; /* failure */

	filename = xmalloc_follow_symlinks(filename);
	if (filename == NULL)
		return -1;

	check_selinux_update_passwd(username);

	/* New passwd file, "/etc/passwd+" for now */
	fnamesfx = xasprintf("%s+", filename);
	sfx_char = &fnamesfx[strlen(fnamesfx)-1];
	username = xasprintf("%s:", username);
	user_len = strlen(username);

	old_fp = fopen(filename, "r+");
	if (!old_fp)
		goto free_mem;
	old_fd = fileno(old_fp);

	selinux_preserve_fcontext(old_fd);

	/* Try to create "/etc/passwd+". Wait if it exists. */
	i = 30;
	do {
		// FIXME: on last iteration try w/o O_EXCL but with O_TRUNC?
		new_fd = open(fnamesfx, O_WRONLY|O_CREAT|O_EXCL, 0600);
		if (new_fd >= 0) goto created;
		if (errno != EEXIST) break;
		usleep(100000); /* 0.1 sec */
	} while (--i);
	bb_perror_msg("cannot create '%s'", fnamesfx);
	goto close_old_fp;

 created:
	if (!fstat(old_fd, &sb)) {
		fchmod(new_fd, sb.st_mode & 0777); /* ignore errors */
		fchown(new_fd, sb.st_uid, sb.st_gid);
	}
	new_fp = fdopen(new_fd, "w");
	if (!new_fp) {
		close(new_fd);
		goto unlink_new;
	}

	/* Backup file is "/etc/passwd-" */
	*sfx_char = '-';
	/* Delete old backup */
	i = (unlink(fnamesfx) && errno != ENOENT);
	/* Create backup as a hardlink to current */
	if (i || link(filename, fnamesfx))
		bb_perror_msg("warning: cannot create backup copy '%s'", fnamesfx);
	*sfx_char = '+';

	/* Lock the password file before updating */
	lock.l_type = F_WRLCK;
	lock.l_whence = SEEK_SET;
	lock.l_start = 0;
	lock.l_len = 0;
	if (fcntl(old_fd, F_SETLK, &lock) < 0)
		bb_perror_msg("warning: cannot lock '%s'", filename);
	lock.l_type = F_UNLCK;

	/* Read current password file, write updated /etc/passwd+ */
	while (1) {
		char *line = xmalloc_fgets(old_fp);
		if (!line) break; /* EOF/error */
		if (strncmp(username, line, user_len) == 0) {
			/* we have a match with "username:"... */
			const char *cp = line + user_len;
			/* now cp -> old passwd, skip it: */
			cp = strchrnul(cp, ':');
			/* now cp -> ':' after old passwd or -> "" */
			fprintf(new_fp, "%s%s%s", username, new_pw, cp);
			cnt++;
		} else
			fputs(line, new_fp);
		free(line);
	}
	fcntl(old_fd, F_SETLK, &lock);

	/* We do want all of them to execute, thus | instead of || */
	if ((ferror(old_fp) | fflush(new_fp) | fsync(new_fd) | fclose(new_fp))
	 || rename(fnamesfx, filename)
	) {
		/* At least one of those failed */
		goto unlink_new;
	}
	ret = cnt; /* whee, success! */

 unlink_new:
	if (ret < 0) unlink(fnamesfx);

 close_old_fp:
	fclose(old_fp);

 free_mem:
	free(fnamesfx);
	free((char *)filename);
	free((char *)username);
	return ret;
}