freebsd-skq/sys/kern/tty_pts.c
ed 4f08ecd7ed Rename st_*timespec fields to st_*tim for POSIX 2008 compliance.
A nice thing about POSIX 2008 is that it finally standardizes a way to
obtain file access/modification/change times in sub-second precision,
namely using struct timespec, which we already have for a very long
time. Unfortunately POSIX uses different names.

This commit adds compatibility macros, so existing code should still
build properly. Also change all source code in the kernel to work
without any of the compatibility macros. This makes it all a less
ambiguous.

I am also renaming st_birthtime to st_birthtim, even though it was a
local extension anyway. It seems Cygwin also has a st_birthtim.
2010-03-28 13:13:22 +00:00

827 lines
18 KiB
C

/*-
* Copyright (c) 2008 Ed Schouten <ed@FreeBSD.org>
* All rights reserved.
*
* Portions of this software were developed under sponsorship from Snow
* B.V., the Netherlands.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#include <sys/cdefs.h>
__FBSDID("$FreeBSD$");
/* Add compatibility bits for FreeBSD. */
#define PTS_COMPAT
/* Add pty(4) compat bits. */
#define PTS_EXTERNAL
/* Add bits to make Linux binaries work. */
#define PTS_LINUX
#include <sys/param.h>
#include <sys/lock.h>
#include <sys/condvar.h>
#include <sys/conf.h>
#include <sys/fcntl.h>
#include <sys/file.h>
#include <sys/filedesc.h>
#include <sys/filio.h>
#include <sys/kernel.h>
#include <sys/limits.h>
#include <sys/malloc.h>
#include <sys/poll.h>
#include <sys/proc.h>
#include <sys/resourcevar.h>
#include <sys/serial.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/syscallsubr.h>
#include <sys/sysctl.h>
#include <sys/sysent.h>
#include <sys/sysproto.h>
#include <sys/systm.h>
#include <sys/tty.h>
#include <sys/ttycom.h>
#include <machine/stdarg.h>
/*
* Our utmp(5) format is limited to 8-byte TTY line names. This means
* we can at most allocate 1000 pseudo-terminals ("pts/999"). Allow
* users to increase this number, assuming they have manually increased
* UT_LINESIZE.
*/
static struct unrhdr *pts_pool;
static MALLOC_DEFINE(M_PTS, "pts", "pseudo tty device");
/*
* Per-PTS structure.
*
* List of locks
* (t) locked by tty_lock()
* (c) const until freeing
*/
struct pts_softc {
int pts_unit; /* (c) Device unit number. */
unsigned int pts_flags; /* (t) Device flags. */
#define PTS_PKT 0x1 /* Packet mode. */
#define PTS_FINISHED 0x2 /* Return errors on read()/write(). */
char pts_pkt; /* (t) Unread packet mode data. */
struct cv pts_inwait; /* (t) Blocking write() on master. */
struct selinfo pts_inpoll; /* (t) Select queue for write(). */
struct cv pts_outwait; /* (t) Blocking read() on master. */
struct selinfo pts_outpoll; /* (t) Select queue for read(). */
#ifdef PTS_EXTERNAL
struct cdev *pts_cdev; /* (c) Master device node. */
#endif /* PTS_EXTERNAL */
struct uidinfo *pts_uidinfo; /* (c) Resource limit. */
};
/*
* Controller-side file operations.
*/
static int
ptsdev_read(struct file *fp, struct uio *uio, struct ucred *active_cred,
int flags, struct thread *td)
{
struct tty *tp = fp->f_data;
struct pts_softc *psc = tty_softc(tp);
int error = 0;
char pkt;
if (uio->uio_resid == 0)
return (0);
tty_lock(tp);
for (;;) {
/*
* Implement packet mode. When packet mode is turned on,
* the first byte contains a bitmask of events that
* occured (start, stop, flush, window size, etc).
*/
if (psc->pts_flags & PTS_PKT && psc->pts_pkt) {
pkt = psc->pts_pkt;
psc->pts_pkt = 0;
tty_unlock(tp);
error = ureadc(pkt, uio);
return (error);
}
/*
* Transmit regular data.
*
* XXX: We shouldn't use ttydisc_getc_poll()! Even
* though in this implementation, there is likely going
* to be data, we should just call ttydisc_getc_uio()
* and use its return value to sleep.
*/
if (ttydisc_getc_poll(tp)) {
if (psc->pts_flags & PTS_PKT) {
/*
* XXX: Small race. Fortunately PTY
* consumers aren't multithreaded.
*/
tty_unlock(tp);
error = ureadc(TIOCPKT_DATA, uio);
if (error)
return (error);
tty_lock(tp);
}
error = ttydisc_getc_uio(tp, uio);
break;
}
/* Maybe the device isn't used anyway. */
if (psc->pts_flags & PTS_FINISHED)
break;
/* Wait for more data. */
if (fp->f_flag & O_NONBLOCK) {
error = EWOULDBLOCK;
break;
}
error = cv_wait_sig(&psc->pts_outwait, tp->t_mtx);
if (error != 0)
break;
}
tty_unlock(tp);
return (error);
}
static int
ptsdev_write(struct file *fp, struct uio *uio, struct ucred *active_cred,
int flags, struct thread *td)
{
struct tty *tp = fp->f_data;
struct pts_softc *psc = tty_softc(tp);
char ib[256], *ibstart;
size_t iblen, rintlen;
int error = 0;
if (uio->uio_resid == 0)
return (0);
for (;;) {
ibstart = ib;
iblen = MIN(uio->uio_resid, sizeof ib);
error = uiomove(ib, iblen, uio);
tty_lock(tp);
if (error != 0) {
iblen = 0;
goto done;
}
/*
* When possible, avoid the slow path. rint_bypass()
* copies all input to the input queue at once.
*/
MPASS(iblen > 0);
do {
rintlen = ttydisc_rint_simple(tp, ibstart, iblen);
ibstart += rintlen;
iblen -= rintlen;
if (iblen == 0) {
/* All data written. */
break;
}
/* Maybe the device isn't used anyway. */
if (psc->pts_flags & PTS_FINISHED) {
error = EIO;
goto done;
}
/* Wait for more data. */
if (fp->f_flag & O_NONBLOCK) {
error = EWOULDBLOCK;
goto done;
}
/* Wake up users on the slave side. */
ttydisc_rint_done(tp);
error = cv_wait_sig(&psc->pts_inwait, tp->t_mtx);
if (error != 0)
goto done;
} while (iblen > 0);
if (uio->uio_resid == 0)
break;
tty_unlock(tp);
}
done: ttydisc_rint_done(tp);
tty_unlock(tp);
/*
* Don't account for the part of the buffer that we couldn't
* pass to the TTY.
*/
uio->uio_resid += iblen;
return (error);
}
static int
ptsdev_truncate(struct file *fp, off_t length, struct ucred *active_cred,
struct thread *td)
{
return (EINVAL);
}
static int
ptsdev_ioctl(struct file *fp, u_long cmd, void *data,
struct ucred *active_cred, struct thread *td)
{
struct tty *tp = fp->f_data;
struct pts_softc *psc = tty_softc(tp);
int error = 0, sig;
switch (cmd) {
case FIONBIO:
/* This device supports non-blocking operation. */
return (0);
case FIONREAD:
tty_lock(tp);
if (psc->pts_flags & PTS_FINISHED) {
/* Force read() to be called. */
*(int *)data = 1;
} else {
*(int *)data = ttydisc_getc_poll(tp);
}
tty_unlock(tp);
return (0);
case FIODGNAME: {
struct fiodgname_arg *fgn;
const char *p;
int i;
/* Reverse device name lookups, for ptsname() and ttyname(). */
fgn = data;
p = tty_devname(tp);
i = strlen(p) + 1;
if (i > fgn->len)
return (EINVAL);
return copyout(p, fgn->buf, i);
}
/*
* We need to implement TIOCGPGRP and TIOCGSID here again. When
* called on the pseudo-terminal master, it should not check if
* the terminal is the foreground terminal of the calling
* process.
*
* TIOCGETA is also implemented here. Various Linux PTY routines
* often call isatty(), which is implemented by tcgetattr().
*/
#ifdef PTS_LINUX
case TIOCGETA:
/* Obtain terminal flags through tcgetattr(). */
tty_lock(tp);
*(struct termios*)data = tp->t_termios;
tty_unlock(tp);
return (0);
#endif /* PTS_LINUX */
case TIOCSETAF:
case TIOCSETAW:
/*
* We must make sure we turn tcsetattr() calls of TCSAFLUSH and
* TCSADRAIN into something different. If an application would
* call TCSAFLUSH or TCSADRAIN on the master descriptor, it may
* deadlock waiting for all data to be read.
*/
cmd = TIOCSETA;
break;
#if defined(PTS_COMPAT) || defined(PTS_LINUX)
case TIOCGPTN:
/*
* Get the device unit number.
*/
if (psc->pts_unit < 0)
return (ENOTTY);
*(unsigned int *)data = psc->pts_unit;
return (0);
#endif /* PTS_COMPAT || PTS_LINUX */
case TIOCGPGRP:
/* Get the foreground process group ID. */
tty_lock(tp);
if (tp->t_pgrp != NULL)
*(int *)data = tp->t_pgrp->pg_id;
else
*(int *)data = NO_PID;
tty_unlock(tp);
return (0);
case TIOCGSID:
/* Get the session leader process ID. */
tty_lock(tp);
if (tp->t_session == NULL)
error = ENOTTY;
else
*(int *)data = tp->t_session->s_sid;
tty_unlock(tp);
return (error);
case TIOCPTMASTER:
/* Yes, we are a pseudo-terminal master. */
return (0);
case TIOCSIG:
/* Signal the foreground process group. */
sig = *(int *)data;
if (sig < 1 || sig >= NSIG)
return (EINVAL);
tty_lock(tp);
tty_signal_pgrp(tp, sig);
tty_unlock(tp);
return (0);
case TIOCPKT:
/* Enable/disable packet mode. */
tty_lock(tp);
if (*(int *)data)
psc->pts_flags |= PTS_PKT;
else
psc->pts_flags &= ~PTS_PKT;
tty_unlock(tp);
return (0);
}
/* Just redirect this ioctl to the slave device. */
tty_lock(tp);
error = tty_ioctl(tp, cmd, data, fp->f_flag, td);
tty_unlock(tp);
if (error == ENOIOCTL)
error = ENOTTY;
return (error);
}
static int
ptsdev_poll(struct file *fp, int events, struct ucred *active_cred,
struct thread *td)
{
struct tty *tp = fp->f_data;
struct pts_softc *psc = tty_softc(tp);
int revents = 0;
tty_lock(tp);
if (psc->pts_flags & PTS_FINISHED) {
/* Slave device is not opened. */
tty_unlock(tp);
return ((events & (POLLIN|POLLRDNORM)) | POLLHUP);
}
if (events & (POLLIN|POLLRDNORM)) {
/* See if we can getc something. */
if (ttydisc_getc_poll(tp) ||
(psc->pts_flags & PTS_PKT && psc->pts_pkt))
revents |= events & (POLLIN|POLLRDNORM);
}
if (events & (POLLOUT|POLLWRNORM)) {
/* See if we can rint something. */
if (ttydisc_rint_poll(tp))
revents |= events & (POLLOUT|POLLWRNORM);
}
/*
* No need to check for POLLHUP here. This device cannot be used
* as a callout device, which means we always have a carrier,
* because the master is.
*/
if (revents == 0) {
/*
* This code might look misleading, but the naming of
* poll events on this side is the opposite of the slave
* device.
*/
if (events & (POLLIN|POLLRDNORM))
selrecord(td, &psc->pts_outpoll);
if (events & (POLLOUT|POLLWRNORM))
selrecord(td, &psc->pts_inpoll);
}
tty_unlock(tp);
return (revents);
}
/*
* kqueue support.
*/
static void
pts_kqops_read_detach(struct knote *kn)
{
struct file *fp = kn->kn_fp;
struct tty *tp = fp->f_data;
struct pts_softc *psc = tty_softc(tp);
knlist_remove(&psc->pts_outpoll.si_note, kn, 0);
}
static int
pts_kqops_read_event(struct knote *kn, long hint)
{
struct file *fp = kn->kn_fp;
struct tty *tp = fp->f_data;
struct pts_softc *psc = tty_softc(tp);
if (psc->pts_flags & PTS_FINISHED) {
kn->kn_flags |= EV_EOF;
return (1);
} else {
kn->kn_data = ttydisc_getc_poll(tp);
return (kn->kn_data > 0);
}
}
static void
pts_kqops_write_detach(struct knote *kn)
{
struct file *fp = kn->kn_fp;
struct tty *tp = fp->f_data;
struct pts_softc *psc = tty_softc(tp);
knlist_remove(&psc->pts_inpoll.si_note, kn, 0);
}
static int
pts_kqops_write_event(struct knote *kn, long hint)
{
struct file *fp = kn->kn_fp;
struct tty *tp = fp->f_data;
struct pts_softc *psc = tty_softc(tp);
if (psc->pts_flags & PTS_FINISHED) {
kn->kn_flags |= EV_EOF;
return (1);
} else {
kn->kn_data = ttydisc_rint_poll(tp);
return (kn->kn_data > 0);
}
}
static struct filterops pts_kqops_read = {
.f_isfd = 1,
.f_detach = pts_kqops_read_detach,
.f_event = pts_kqops_read_event,
};
static struct filterops pts_kqops_write = {
.f_isfd = 1,
.f_detach = pts_kqops_write_detach,
.f_event = pts_kqops_write_event,
};
static int
ptsdev_kqfilter(struct file *fp, struct knote *kn)
{
struct tty *tp = fp->f_data;
struct pts_softc *psc = tty_softc(tp);
int error = 0;
tty_lock(tp);
switch (kn->kn_filter) {
case EVFILT_READ:
kn->kn_fop = &pts_kqops_read;
knlist_add(&psc->pts_outpoll.si_note, kn, 1);
break;
case EVFILT_WRITE:
kn->kn_fop = &pts_kqops_write;
knlist_add(&psc->pts_inpoll.si_note, kn, 1);
break;
default:
error = EINVAL;
break;
}
tty_unlock(tp);
return (error);
}
static int
ptsdev_stat(struct file *fp, struct stat *sb, struct ucred *active_cred,
struct thread *td)
{
struct tty *tp = fp->f_data;
#ifdef PTS_EXTERNAL
struct pts_softc *psc = tty_softc(tp);
#endif /* PTS_EXTERNAL */
struct cdev *dev = tp->t_dev;
/*
* According to POSIX, we must implement an fstat(). This also
* makes this implementation compatible with Linux binaries,
* because Linux calls fstat() on the pseudo-terminal master to
* obtain st_rdev.
*
* XXX: POSIX also mentions we must fill in st_dev, but how?
*/
bzero(sb, sizeof *sb);
#ifdef PTS_EXTERNAL
if (psc->pts_cdev != NULL)
sb->st_ino = sb->st_rdev = dev2udev(psc->pts_cdev);
else
#endif /* PTS_EXTERNAL */
sb->st_ino = sb->st_rdev = tty_udev(tp);
sb->st_atim = dev->si_atime;
sb->st_ctim = dev->si_ctime;
sb->st_mtim = dev->si_mtime;
sb->st_uid = dev->si_uid;
sb->st_gid = dev->si_gid;
sb->st_mode = dev->si_mode | S_IFCHR;
return (0);
}
static int
ptsdev_close(struct file *fp, struct thread *td)
{
struct tty *tp = fp->f_data;
/* Deallocate TTY device. */
tty_lock(tp);
tty_rel_gone(tp);
return (0);
}
static struct fileops ptsdev_ops = {
.fo_read = ptsdev_read,
.fo_write = ptsdev_write,
.fo_truncate = ptsdev_truncate,
.fo_ioctl = ptsdev_ioctl,
.fo_poll = ptsdev_poll,
.fo_kqfilter = ptsdev_kqfilter,
.fo_stat = ptsdev_stat,
.fo_close = ptsdev_close,
.fo_flags = DFLAG_PASSABLE,
};
/*
* Driver-side hooks.
*/
static void
ptsdrv_outwakeup(struct tty *tp)
{
struct pts_softc *psc = tty_softc(tp);
cv_broadcast(&psc->pts_outwait);
selwakeup(&psc->pts_outpoll);
KNOTE_LOCKED(&psc->pts_outpoll.si_note, 0);
}
static void
ptsdrv_inwakeup(struct tty *tp)
{
struct pts_softc *psc = tty_softc(tp);
cv_broadcast(&psc->pts_inwait);
selwakeup(&psc->pts_inpoll);
KNOTE_LOCKED(&psc->pts_inpoll.si_note, 0);
}
static int
ptsdrv_open(struct tty *tp)
{
struct pts_softc *psc = tty_softc(tp);
psc->pts_flags &= ~PTS_FINISHED;
return (0);
}
static void
ptsdrv_close(struct tty *tp)
{
struct pts_softc *psc = tty_softc(tp);
/* Wake up any blocked readers/writers. */
psc->pts_flags |= PTS_FINISHED;
ptsdrv_outwakeup(tp);
ptsdrv_inwakeup(tp);
}
static void
ptsdrv_pktnotify(struct tty *tp, char event)
{
struct pts_softc *psc = tty_softc(tp);
/*
* Clear conflicting flags.
*/
switch (event) {
case TIOCPKT_STOP:
psc->pts_pkt &= ~TIOCPKT_START;
break;
case TIOCPKT_START:
psc->pts_pkt &= ~TIOCPKT_STOP;
break;
case TIOCPKT_NOSTOP:
psc->pts_pkt &= ~TIOCPKT_DOSTOP;
break;
case TIOCPKT_DOSTOP:
psc->pts_pkt &= ~TIOCPKT_NOSTOP;
break;
}
psc->pts_pkt |= event;
ptsdrv_outwakeup(tp);
}
static void
ptsdrv_free(void *softc)
{
struct pts_softc *psc = softc;
/* Make device number available again. */
if (psc->pts_unit >= 0)
free_unr(pts_pool, psc->pts_unit);
chgptscnt(psc->pts_uidinfo, -1, 0);
uifree(psc->pts_uidinfo);
knlist_destroy(&psc->pts_inpoll.si_note);
knlist_destroy(&psc->pts_outpoll.si_note);
#ifdef PTS_EXTERNAL
/* Destroy master device as well. */
if (psc->pts_cdev != NULL)
destroy_dev_sched(psc->pts_cdev);
#endif /* PTS_EXTERNAL */
free(psc, M_PTS);
}
static struct ttydevsw pts_class = {
.tsw_flags = TF_NOPREFIX,
.tsw_outwakeup = ptsdrv_outwakeup,
.tsw_inwakeup = ptsdrv_inwakeup,
.tsw_open = ptsdrv_open,
.tsw_close = ptsdrv_close,
.tsw_pktnotify = ptsdrv_pktnotify,
.tsw_free = ptsdrv_free,
};
#ifndef PTS_EXTERNAL
static
#endif /* !PTS_EXTERNAL */
int
pts_alloc(int fflags, struct thread *td, struct file *fp)
{
int unit, ok;
struct tty *tp;
struct pts_softc *psc;
struct proc *p = td->td_proc;
struct uidinfo *uid = td->td_ucred->cr_ruidinfo;
/* Resource limiting. */
PROC_LOCK(p);
ok = chgptscnt(uid, 1, lim_cur(p, RLIMIT_NPTS));
PROC_UNLOCK(p);
if (!ok)
return (EAGAIN);
/* Try to allocate a new pts unit number. */
unit = alloc_unr(pts_pool);
if (unit < 0) {
chgptscnt(uid, -1, 0);
return (EAGAIN);
}
/* Allocate TTY and softc. */
psc = malloc(sizeof(struct pts_softc), M_PTS, M_WAITOK|M_ZERO);
cv_init(&psc->pts_inwait, "ptsin");
cv_init(&psc->pts_outwait, "ptsout");
psc->pts_unit = unit;
psc->pts_uidinfo = uid;
uihold(uid);
tp = tty_alloc(&pts_class, psc);
knlist_init_mtx(&psc->pts_inpoll.si_note, tp->t_mtx);
knlist_init_mtx(&psc->pts_outpoll.si_note, tp->t_mtx);
/* Expose the slave device as well. */
tty_makedev(tp, td->td_ucred, "pts/%u", psc->pts_unit);
finit(fp, fflags, DTYPE_PTS, tp, &ptsdev_ops);
return (0);
}
#ifdef PTS_EXTERNAL
int
pts_alloc_external(int fflags, struct thread *td, struct file *fp,
struct cdev *dev, const char *name)
{
int ok;
struct tty *tp;
struct pts_softc *psc;
struct proc *p = td->td_proc;
struct uidinfo *uid = td->td_ucred->cr_ruidinfo;
/* Resource limiting. */
PROC_LOCK(p);
ok = chgptscnt(uid, 1, lim_cur(p, RLIMIT_NPTS));
PROC_UNLOCK(p);
if (!ok)
return (EAGAIN);
/* Allocate TTY and softc. */
psc = malloc(sizeof(struct pts_softc), M_PTS, M_WAITOK|M_ZERO);
cv_init(&psc->pts_inwait, "ptsin");
cv_init(&psc->pts_outwait, "ptsout");
psc->pts_unit = -1;
psc->pts_cdev = dev;
psc->pts_uidinfo = uid;
uihold(uid);
tp = tty_alloc(&pts_class, psc);
knlist_init_mtx(&psc->pts_inpoll.si_note, tp->t_mtx);
knlist_init_mtx(&psc->pts_outpoll.si_note, tp->t_mtx);
/* Expose the slave device as well. */
tty_makedev(tp, td->td_ucred, "%s", name);
finit(fp, fflags, DTYPE_PTS, tp, &ptsdev_ops);
return (0);
}
#endif /* PTS_EXTERNAL */
int
posix_openpt(struct thread *td, struct posix_openpt_args *uap)
{
int error, fd;
struct file *fp;
/*
* POSIX states it's unspecified when other flags are passed. We
* don't allow this.
*/
if (uap->flags & ~(O_RDWR|O_NOCTTY))
return (EINVAL);
error = falloc(td, &fp, &fd);
if (error)
return (error);
/* Allocate the actual pseudo-TTY. */
error = pts_alloc(FFLAGS(uap->flags & O_ACCMODE), td, fp);
if (error != 0) {
fdclose(td->td_proc->p_fd, fp, fd, td);
return (error);
}
/* Pass it back to userspace. */
td->td_retval[0] = fd;
fdrop(fp, td);
return (0);
}
static void
pts_init(void *unused)
{
pts_pool = new_unrhdr(0, INT_MAX, NULL);
}
SYSINIT(pts, SI_SUB_DRIVERS, SI_ORDER_MIDDLE, pts_init, NULL);