Commit 5b0aba61 authored by Niels Möller's avatar Niels Möller
Browse files

Rewritten. Uses the functions in pty-helper.c. Handle utmp and wtmp.

Rev: src/lshd-pty-helper.c:1.1.2.2
parent fff819a9
......@@ -5,10 +5,30 @@
* socket, or socket pair(s) provided as stdin and stdout.
*/
/* lsh, an implementation of the ssh protocol
*
* Copyright (C) 2006 Niels Mller
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#if HAVE_CONFIG_H
#include "config.h"
#endif
#include <assert.h>
#include <errno.h>
#include <stdarg.h>
#include <stdlib.h>
......@@ -18,9 +38,38 @@
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <grp.h>
#include <utmpx.h>
#include "environ.h"
#include "pty-helper.h"
#ifndef GROUP_TTY
#define GROUP_TTY "tty"
#endif
#ifndef GROUP_SYSTEM
#define GROUP_SYSTEM "system"
#endif
/* Includes user, group and other bits, as well as the suid, sgid and
sticky bit. */
#ifndef ACCESS_MASK
#define ACCESS_MASK 07777
#endif
/* Desired tty access bits (rw--w----). Can we gain any portability by
writing S_IRUSR | S_IWUSR | S_IWGRP ? */
#ifndef ACCESS_TTY
#define ACCESS_TTY 0620
#endif
static const char *wtmp_file;
void
static void
die(const char *format, ...)
#if __GNUC___
__attribute__((__format__ (__printf__,1, 2)))
......@@ -28,7 +77,7 @@ die(const char *format, ...)
#endif
;
void
static void
die(const char *format, ...)
{
va_list args;
......@@ -39,146 +88,526 @@ die(const char *format, ...)
exit(EXIT_FAILURE);
}
/* A request is a single type byte, and a SCM_CREDENTIALS control
message. */
struct pty_request
static void
werror(const char *format, ...)
#if __GNUC___
__attribute__((__format__ (__printf__,1, 2)))
#endif
;
static void
werror(const char *format, ...)
{
char type;
pid_t pid;
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
}
/* The state associated with a pty and an utmp entry. */
struct pty_object
{
char free;
/* Non-zero if we have an utmp entry to clean up. */
char active;
/* The client's uid. */
uid_t uid;
gid_t gid;
};
/* The response includes the fd of the master side of the pty pair,
and the name of the slave tty. It is sent as two messages, one with
the fd and the length, and another with the tty name. Since the
protocol is used only locally on a single machine, we send the
length using whatever the native representation of an unsigned
is. */
/* Name of slave tty */
const char *tty;
struct pty_response
struct utmpx entry;
};
struct pty_state
{
/* Master pty */
int fd;
unsigned tty_length;
const char *tty_name;
/* Group that should own the slave tty. */
gid_t tty_gid;
/* Expected user id, or -1 if unset. */
uid_t uid;
unsigned nobjects;
struct pty_object *objects;
};
static int
recv_request(int fd, struct pty_request *request)
static void
init_pty_state(struct pty_state *state, uid_t uid)
{
struct msghdr msg;
struct cmsghdr *cmsg;
struct ucred creds;
struct iovec io;
int res;
/* FIXME: Not portable to assume CMSG_SPACE expands to a constant
expression. */
char buf[CMSG_SPACE(sizeof(creds))];
io.iov_base = &request->type;
io.iov_len = sizeof(request->type);
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
struct group *grp;
/* Points to static area */
grp = getgrnam(GROUP_TTY);
if (!grp)
/* On AIX, tty:s have group "system", not "tty" */
grp = getgrnam(GROUP_SYSTEM);
do
res = recvmsg(fd, &msg, 0);
while (res < 0 && errno == EINTR);
state->tty_gid = grp ? grp->gr_gid : (gid_t) -1;
if (res != 1)
return 0;
state->uid = uid;
cmsg = CMSG_FIRSTHDR(&msg);
state->nobjects = 0;
state->objects = NULL;
}
if (cmsg->cmsg_level == SOL_SOCKET
&& cmsg->cmsg_type == SCM_CREDENTIALS
&& cmsg->cmsg_len == CMSG_LEN(sizeof(creds)))
static struct pty_object *
pty_object_alloc(struct pty_state *state, unsigned *index)
{
struct pty_object *pty = NULL;
unsigned i;
for (i = 0; i < state->nobjects; i++)
if (state->objects[i].free)
{
*index = i;
pty = state->objects + i;
break;
}
if (!pty)
{
/* No alignment guarantees, so use memcpy. */
memcpy(&creds, CMSG_DATA(cmsg), sizeof(creds));
request->pid = creds.pid;
request->uid = creds.uid;
request->gid = creds.gid;
/* Try reallocating */
size_t n = 2*state->nobjects + 10;
void *p = realloc(state->objects, n*sizeof(*state->objects));
if (!p)
return NULL;
state->objects = p;
return 1;
*index = state->nobjects;
pty = state->objects + state->nobjects;
for (i = state->nobjects; i < n; i++)
state->objects[i].free = 1;
state->nobjects = n;
}
return 0;
memset(pty, 0, sizeof(*pty));
return pty;
}
static int
send_response(int fd, const struct pty_response *response)
static void
pty_object_free(struct pty_state *state, unsigned index)
{
struct msghdr msg;
struct cmsghdr *cmsg;
struct iovec io;
int res;
char buf[CMSG_SPACE(sizeof(response->fd))];
io.iov_base = (void *) &response->tty_length;
io.iov_len = sizeof(response->tty_length);
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(response->fd));
assert(index < state->nobjects);
assert(!state->objects[index].free);
free((void *) state->objects[index].tty);
memcpy(CMSG_DATA(cmsg), &response->fd, sizeof(response->fd));
state->objects[index].free = 1;
}
do
res = sendmsg(fd, &msg, 0);
while (res < 0 && errno == EINTR);
/* Sets the permissions on the slave pty suitably for use by USER.
* This function is derived from the grantpt function in
* sysdeps/unix/grantpt.c in glibc-2.1. */
if (res != sizeof(response->tty_length))
return 0;
/* Returns errno value on error */
static int
pty_set_permissions(const char *name, uid_t uid, gid_t gid)
{
struct stat st;
io.iov_base = (void *) response->tty_name;
io.iov_len = response->tty_length;
if (stat(name, &st) < 0)
return errno;
msg.msg_control = NULL;
msg.msg_controllen = 0;
/* Make sure that the user owns the device. */
if (st.st_uid == uid)
uid = -1;
if (st.st_gid == gid)
gid = -1;
do
res = sendmsg(fd, &msg, 0);
while (res < 0 && errno == EINTR);
return (res == response->tty_length);
if (uid != (uid_t) -1 || gid != (gid_t) -1)
if (chown(name, uid, gid) < 0)
return errno;
/* Make sure the permission mode is set to readable and writable
* by the owner, and writable by the group. */
if ( (st.st_mode & ACCESS_MASK) != ACCESS_TTY
&& chmod(name, ACCESS_TTY) < 0)
return errno;
/* Everything is fine */
return 0;
}
static int
process_request(const struct pty_request *request,
struct pty_response *response)
strprefix_p(const char *prefix, const char *s)
{
return 0;
unsigned i;
for (i = 0; prefix[i]; i++)
if (prefix[i] != s[i])
return 0;
return 1;
}
#define CP(dst, src) (strncpy((dst), (src), sizeof(dst)))
#define CPN(dst, offset, src) \
(strncpy((dst)+(offset), (src), sizeof(dst)-(offset)))
static void
process_request(struct pty_state *state,
const struct pty_message *request,
struct pty_message *response)
{
struct pty_object *pty;
response->header.type = 0;
response->header.ref = -1;
response->header.length = 0;
response->data = NULL;
response->has_creds = 0;
response->fd = -1;
/* Require credentials for all requests */
if (!request->has_creds)
{
werror("Missing credentials.\n");
response->header.type = EPERM;
return;
}
if (state->uid != (uid_t) -1
&& request->creds.uid != state->uid)
{
response->header.type = EPERM;
return;
}
if (request->header.ref == -1)
pty = NULL;
else if (request->header.ref < 0
|| (unsigned) request->header.ref >= state->nobjects)
{
response->header.type = EINVAL;
return;
}
else
{
pty = &state->objects[request->header.ref];
if (pty->free)
{
response->header.type = EINVAL;
return;
}
}
switch(request->header.type)
{
case PTY_REQUEST_CREATE:
werror("PTY_REQUEST_CREATE\n");
if (request->header.ref != -1)
{
response->header.type = EINVAL;
}
else
{
unsigned index;
pty = pty_object_alloc(state, &index);
if (!pty)
{
response->header.type = ENOMEM;
}
else
{
pty->uid = request->creds.uid;
if (request->fd != -1)
{
/* Client can supply the master fd, so we can get
the slave tty name from there. */
char *tty = ptsname(request->fd);
if (tty)
tty = strdup(tty);
if (!tty)
{
response->header.type = errno;
pty_object_free(state, index);
return;
}
pty->tty = tty;
/* FIXME: We could stat the tty and check ownership? */
}
response->header.ref = index;
}
}
break;
case PTY_REQUEST_MASTER:
werror("PTY_REQUEST_MASTER\n");
response->header.type = EOPNOTSUPP;
/* Useful only for old bsd-style tty allocation. When using
/devptmx and grantpt, it's better to let the client create
the pty, since it will get the right ownership from the
start. */
#if 0
if (request->header.ref != -1)
{
response->header.type = EINVAL;
}
else
{
unsigned index;
const char *tty;
pty = pty_object_alloc(state, &index);
if (!pty)
{
response->header.type = ENOMEM;
return;
}
response->fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
if (response->fd < 0)
{
pty_object_free(state, index);
response->header.type = errno;
return;
}
tty = ptsname(response->fd);
if (!tty)
{
response->header.type = errno;
fail_and_close:
pty_object_free(state, index);
close(response->fd);
response->fd = -1;
return;
}
/* Copy, since ptsname returns a statically allocated value */
pty->tty = strdup(tty);
if (!pty->tty)
{
response->header.type = errno;
goto fail_and_close;
}
pty->uid = request->creds.uid;
if (pty->uid == getuid())
{
/* Use standard grantpt call */
if (grantpt(response->fd) < 0)
{
response->header.type = errno;
goto fail_and_close;
}
}
else
{
gid_t gid = state->tty_gid;
if (gid == (gid_t) -1)
gid = request->creds.gid;
response->header.type
= pty_set_permissions(pty->tty, pty->uid, gid);
if (response->header.type)
goto fail_and_close;
}
if (unlockpt(response->fd < 0))
{
response->header.type = errno;
goto fail_and_close;
}
response->header.ref = index;
response->length = strlen(pty->tty);
response->data = pty->tty;
}
#endif
break;
case PTY_REQUEST_LOGIN:
werror("PTY_REQUEST_LOGIN\n");
if (!pty)
{
response->header.type = EINVAL;
}
else if (request->creds.uid != pty->uid)
{
response->header.type = EPERM;
}
else if (pty->active)
{
response->header.type = EEXIST;
}
else
{
response->header.ref = request->header.ref;
memset(&pty->entry, 0, sizeof(pty->entry));
pty->entry.ut_type = USER_PROCESS;
pty->entry.ut_pid = request->creds.pid;
#if 0
CP(pty->entry.ut_user, state->user_name);
#endif
gettimeofday(&pty->entry.ut_tv, NULL);
if (pty->tty)
{
/* Set tty-related fields, and update utmp */
const char *line;
if (strprefix_p("/dev/", pty->tty))
line = pty->tty + 5;
else
line = pty->tty;
CP(pty->entry.ut_line, line);
if (strprefix_p("pts/", line))
{
pty->entry.ut_id[0] = 'p';
CPN(pty->entry.ut_id, 1, line + 4);
}
else if (strprefix_p("tty", line))
CP(pty->entry.ut_id, line + 3);
else
CP(pty->entry.ut_id, line);
setutxent();
if (!pututxline(&pty->entry))
werror("pututxline failed.\n");
}
updwtmpx(wtmp_file, &pty->entry);
pty->active = 1;
break;
}
case PTY_REQUEST_LOGOUT:
werror("PTY_REQUEST_LOGOUT\n");
if (!pty)
{
response->header.type = EINVAL;
}
else
{
if (pty->active)
{
pty->entry.ut_type = DEAD_PROCESS;
gettimeofday(&pty->entry.ut_tv, NULL);
if (pty->tty)
{
setutxent();
if (!pututxline(&pty->entry))
werror("pututxline failed.\n");
}
updwtmpx(wtmp_file, &pty->entry);
}
pty_object_free(state, request->header.ref);
}
break;
case PTY_REQUEST_DESTROY:
werror("PTY_REQUEST_DESTROY\n");
if (!pty)
{
response->header.type = EINVAL;
}
else
{
/* This request shouldn't rellay be used for "active" ptys,
i.e., if we have an utmp entry to clean up. */
pty_object_free(state, request->header.ref);
}
break;
default:
response->header.type = EINVAL;
break;
}
}