Changeset View
Changeset View
Standalone View
Standalone View
auth-pam.c
Show First 20 Lines • Show All 50 Lines • ▼ Show 20 Lines | |||||
#include <sys/types.h> | #include <sys/types.h> | ||||
#include <sys/stat.h> | #include <sys/stat.h> | ||||
#include <sys/wait.h> | #include <sys/wait.h> | ||||
#include <errno.h> | #include <errno.h> | ||||
#include <signal.h> | #include <signal.h> | ||||
#include <stdarg.h> | #include <stdarg.h> | ||||
#include <stdlib.h> | |||||
#include <string.h> | #include <string.h> | ||||
#include <unistd.h> | #include <unistd.h> | ||||
#ifdef USE_PAM | #ifdef USE_PAM | ||||
#if defined(HAVE_SECURITY_PAM_APPL_H) | #if defined(HAVE_SECURITY_PAM_APPL_H) | ||||
#include <security/pam_appl.h> | #include <security/pam_appl.h> | ||||
#elif defined (HAVE_PAM_PAM_APPL_H) | #elif defined (HAVE_PAM_PAM_APPL_H) | ||||
#include <pam/pam_appl.h> | #include <pam/pam_appl.h> | ||||
Show All 27 Lines | |||||
#include "canohost.h" | #include "canohost.h" | ||||
#include "log.h" | #include "log.h" | ||||
#include "msg.h" | #include "msg.h" | ||||
#include "packet.h" | #include "packet.h" | ||||
#include "misc.h" | #include "misc.h" | ||||
#include "servconf.h" | #include "servconf.h" | ||||
#include "ssh2.h" | #include "ssh2.h" | ||||
#include "auth-options.h" | #include "auth-options.h" | ||||
#include "misc.h" | |||||
#ifdef GSSAPI | #ifdef GSSAPI | ||||
#include "ssh-gss.h" | #include "ssh-gss.h" | ||||
#endif | #endif | ||||
#include "monitor_wrap.h" | #include "monitor_wrap.h" | ||||
extern ServerOptions options; | extern ServerOptions options; | ||||
extern struct sshbuf *loginmsg; | extern struct sshbuf *loginmsg; | ||||
extern u_int utmp_len; | extern u_int utmp_len; | ||||
Show All 35 Lines | |||||
static struct pam_ctxt *cleanup_ctxt; | static struct pam_ctxt *cleanup_ctxt; | ||||
#ifndef UNSUPPORTED_POSIX_THREADS_HACK | #ifndef UNSUPPORTED_POSIX_THREADS_HACK | ||||
/* | /* | ||||
* Simulate threads with processes. | * Simulate threads with processes. | ||||
*/ | */ | ||||
static int sshpam_thread_status = -1; | static int sshpam_thread_status = -1; | ||||
static mysig_t sshpam_oldsig; | static sshsig_t sshpam_oldsig; | ||||
static void | static void | ||||
sshpam_sigchld_handler(int sig) | sshpam_sigchld_handler(int sig) | ||||
{ | { | ||||
signal(SIGCHLD, SIG_DFL); | ssh_signal(SIGCHLD, SIG_DFL); | ||||
if (cleanup_ctxt == NULL) | if (cleanup_ctxt == NULL) | ||||
return; /* handler called after PAM cleanup, shouldn't happen */ | return; /* handler called after PAM cleanup, shouldn't happen */ | ||||
if (waitpid(cleanup_ctxt->pam_thread, &sshpam_thread_status, WNOHANG) | if (waitpid(cleanup_ctxt->pam_thread, &sshpam_thread_status, WNOHANG) | ||||
<= 0) { | <= 0) { | ||||
/* PAM thread has not exitted, privsep slave must have */ | /* PAM thread has not exitted, privsep slave must have */ | ||||
kill(cleanup_ctxt->pam_thread, SIGTERM); | kill(cleanup_ctxt->pam_thread, SIGTERM); | ||||
while (waitpid(cleanup_ctxt->pam_thread, | while (waitpid(cleanup_ctxt->pam_thread, | ||||
&sshpam_thread_status, 0) == -1) { | &sshpam_thread_status, 0) == -1) { | ||||
Show All 25 Lines | |||||
{ | { | ||||
pid_t pid; | pid_t pid; | ||||
struct pam_ctxt *ctx = arg; | struct pam_ctxt *ctx = arg; | ||||
sshpam_thread_status = -1; | sshpam_thread_status = -1; | ||||
switch ((pid = fork())) { | switch ((pid = fork())) { | ||||
case -1: | case -1: | ||||
error("fork(): %s", strerror(errno)); | error("fork(): %s", strerror(errno)); | ||||
return (-1); | return errno; | ||||
case 0: | case 0: | ||||
close(ctx->pam_psock); | close(ctx->pam_psock); | ||||
ctx->pam_psock = -1; | ctx->pam_psock = -1; | ||||
thread_start(arg); | thread_start(arg); | ||||
_exit(1); | _exit(1); | ||||
default: | default: | ||||
*thread = pid; | *thread = pid; | ||||
close(ctx->pam_csock); | close(ctx->pam_csock); | ||||
ctx->pam_csock = -1; | ctx->pam_csock = -1; | ||||
sshpam_oldsig = signal(SIGCHLD, sshpam_sigchld_handler); | sshpam_oldsig = ssh_signal(SIGCHLD, sshpam_sigchld_handler); | ||||
return (0); | return (0); | ||||
} | } | ||||
} | } | ||||
static int | static int | ||||
pthread_cancel(sp_pthread_t thread) | pthread_cancel(sp_pthread_t thread) | ||||
{ | { | ||||
signal(SIGCHLD, sshpam_oldsig); | ssh_signal(SIGCHLD, sshpam_oldsig); | ||||
return (kill(thread, SIGTERM)); | return (kill(thread, SIGTERM)); | ||||
} | } | ||||
/* ARGSUSED */ | /* ARGSUSED */ | ||||
static int | static int | ||||
pthread_join(sp_pthread_t thread, void **value) | pthread_join(sp_pthread_t thread, void **value) | ||||
{ | { | ||||
int status; | int status; | ||||
if (sshpam_thread_status != -1) | if (sshpam_thread_status != -1) | ||||
return (sshpam_thread_status); | return (sshpam_thread_status); | ||||
signal(SIGCHLD, sshpam_oldsig); | ssh_signal(SIGCHLD, sshpam_oldsig); | ||||
while (waitpid(thread, &status, 0) == -1) { | while (waitpid(thread, &status, 0) == -1) { | ||||
if (errno == EINTR) | if (errno == EINTR) | ||||
continue; | continue; | ||||
fatal("%s: waitpid: %s", __func__, strerror(errno)); | fatal("%s: waitpid: %s", __func__, strerror(errno)); | ||||
} | } | ||||
return (status); | return (status); | ||||
} | } | ||||
#endif | #endif | ||||
Show All 14 Lines | |||||
static char *sshpam_conninfo = NULL; | static char *sshpam_conninfo = NULL; | ||||
/* Some PAM implementations don't implement this */ | /* Some PAM implementations don't implement this */ | ||||
#ifndef HAVE_PAM_GETENVLIST | #ifndef HAVE_PAM_GETENVLIST | ||||
static char ** | static char ** | ||||
pam_getenvlist(pam_handle_t *pamh) | pam_getenvlist(pam_handle_t *pamh) | ||||
{ | { | ||||
/* | /* | ||||
* XXX - If necessary, we can still support envrionment passing | * XXX - If necessary, we can still support environment passing | ||||
* for platforms without pam_getenvlist by searching for known | * for platforms without pam_getenvlist by searching for known | ||||
* env vars (e.g. KRB5CCNAME) from the PAM environment. | * env vars (e.g. KRB5CCNAME) from the PAM environment. | ||||
*/ | */ | ||||
return NULL; | return NULL; | ||||
} | } | ||||
#endif | #endif | ||||
#ifndef HAVE_PAM_PUTENV | |||||
static int | |||||
pam_putenv(pam_handle_t *pamh, const char *name_value) | |||||
{ | |||||
return PAM_SUCCESS; | |||||
} | |||||
#endif /* HAVE_PAM_PUTENV */ | |||||
/* | /* | ||||
* Some platforms, notably Solaris, do not enforce password complexity | * Some platforms, notably Solaris, do not enforce password complexity | ||||
* rules during pam_chauthtok() if the real uid of the calling process | * rules during pam_chauthtok() if the real uid of the calling process | ||||
* is 0, on the assumption that it's being called by "passwd" run by root. | * is 0, on the assumption that it's being called by "passwd" run by root. | ||||
* This wraps pam_chauthtok and sets/restore the real uid so PAM will do | * This wraps pam_chauthtok and sets/restore the real uid so PAM will do | ||||
* the right thing. | * the right thing. | ||||
*/ | */ | ||||
#ifdef SSHPAM_CHAUTHTOK_NEEDS_RUID | #ifdef SSHPAM_CHAUTHTOK_NEEDS_RUID | ||||
Show All 9 Lines | sshpam_chauthtok_ruid(pam_handle_t *pamh, int flags) | ||||
result = pam_chauthtok(pamh, flags); | result = pam_chauthtok(pamh, flags); | ||||
if (setreuid(0, -1) == -1) | if (setreuid(0, -1) == -1) | ||||
fatal("%s: setreuid failed: %s", __func__, strerror(errno)); | fatal("%s: setreuid failed: %s", __func__, strerror(errno)); | ||||
return result; | return result; | ||||
} | } | ||||
# define pam_chauthtok(a,b) (sshpam_chauthtok_ruid((a), (b))) | # define pam_chauthtok(a,b) (sshpam_chauthtok_ruid((a), (b))) | ||||
#endif | #endif | ||||
void | static void | ||||
sshpam_password_change_required(int reqd) | sshpam_password_change_required(int reqd) | ||||
{ | { | ||||
extern struct sshauthopt *auth_opts; | extern struct sshauthopt *auth_opts; | ||||
static int saved_port, saved_agent, saved_x11; | static int saved_port, saved_agent, saved_x11; | ||||
debug3("%s %d", __func__, reqd); | debug3("%s %d", __func__, reqd); | ||||
if (sshpam_authctxt == NULL) | if (sshpam_authctxt == NULL) | ||||
fatal("%s: PAM authctxt not initialized", __func__); | fatal("%s: PAM authctxt not initialized", __func__); | ||||
▲ Show 20 Lines • Show All 52 Lines • ▼ Show 20 Lines | #ifndef UNSUPPORTED_POSIX_THREADS_HACK | ||||
/* Import PAM environment from subprocess */ | /* Import PAM environment from subprocess */ | ||||
if ((r = sshbuf_get_u32(b, &num_env)) != 0) | if ((r = sshbuf_get_u32(b, &num_env)) != 0) | ||||
fatal("%s: buffer error: %s", __func__, ssh_err(r)); | fatal("%s: buffer error: %s", __func__, ssh_err(r)); | ||||
debug("PAM: num PAM env strings %d", num_env); | debug("PAM: num PAM env strings %d", num_env); | ||||
for (i = 0; i < num_env; i++) { | for (i = 0; i < num_env; i++) { | ||||
if ((r = sshbuf_get_cstring(b, &env, NULL)) != 0) | if ((r = sshbuf_get_cstring(b, &env, NULL)) != 0) | ||||
fatal("%s: buffer error: %s", __func__, ssh_err(r)); | fatal("%s: buffer error: %s", __func__, ssh_err(r)); | ||||
#ifdef HAVE_PAM_PUTENV | |||||
/* Errors are not fatal here */ | /* Errors are not fatal here */ | ||||
if ((r = pam_putenv(sshpam_handle, env)) != PAM_SUCCESS) { | if ((r = pam_putenv(sshpam_handle, env)) != PAM_SUCCESS) { | ||||
error("PAM: pam_putenv: %s", | error("PAM: pam_putenv: %s", | ||||
pam_strerror(sshpam_handle, r)); | pam_strerror(sshpam_handle, r)); | ||||
} | } | ||||
#endif | /* | ||||
/* XXX leak env? */ | * XXX this possibly leaks env because it is not documented | ||||
* what pam_putenv() does with it. Does it copy it? Does it | |||||
* take ownweship? We don't know, so it's safest just to leak. | |||||
*/ | |||||
} | } | ||||
#endif | #endif | ||||
} | } | ||||
/* | /* | ||||
* Conversation function for authentication thread. | * Conversation function for authentication thread. | ||||
*/ | */ | ||||
static int | static int | ||||
▲ Show 20 Lines • Show All 151 Lines • ▼ Show 20 Lines | #ifndef UNSUPPORTED_POSIX_THREADS_HACK | ||||
if ((r = sshbuf_put_u32(buffer, sshpam_account_status)) != 0 || | if ((r = sshbuf_put_u32(buffer, sshpam_account_status)) != 0 || | ||||
(r = sshbuf_put_u32(buffer, sshpam_authctxt->force_pwchange)) != 0) | (r = sshbuf_put_u32(buffer, sshpam_authctxt->force_pwchange)) != 0) | ||||
fatal("%s: buffer error: %s", __func__, ssh_err(r)); | fatal("%s: buffer error: %s", __func__, ssh_err(r)); | ||||
/* Export any environment strings set in child */ | /* Export any environment strings set in child */ | ||||
for (i = 0; environ[i] != NULL; i++) { | for (i = 0; environ[i] != NULL; i++) { | ||||
/* Count */ | /* Count */ | ||||
if (i > INT_MAX) | if (i > INT_MAX) | ||||
fatal("%s: too many enviornment strings", __func__); | fatal("%s: too many environment strings", __func__); | ||||
} | } | ||||
if ((r = sshbuf_put_u32(buffer, i)) != 0) | if ((r = sshbuf_put_u32(buffer, i)) != 0) | ||||
fatal("%s: buffer error: %s", __func__, ssh_err(r)); | fatal("%s: buffer error: %s", __func__, ssh_err(r)); | ||||
for (i = 0; environ[i] != NULL; i++) { | for (i = 0; environ[i] != NULL; i++) { | ||||
if ((r = sshbuf_put_cstring(buffer, environ[i])) != 0) | if ((r = sshbuf_put_cstring(buffer, environ[i])) != 0) | ||||
fatal("%s: buffer error: %s", __func__, ssh_err(r)); | fatal("%s: buffer error: %s", __func__, ssh_err(r)); | ||||
} | } | ||||
/* Export any environment strings set by PAM in child */ | /* Export any environment strings set by PAM in child */ | ||||
env_from_pam = pam_getenvlist(sshpam_handle); | env_from_pam = pam_getenvlist(sshpam_handle); | ||||
for (i = 0; env_from_pam != NULL && env_from_pam[i] != NULL; i++) { | for (i = 0; env_from_pam != NULL && env_from_pam[i] != NULL; i++) { | ||||
/* Count */ | /* Count */ | ||||
if (i > INT_MAX) | if (i > INT_MAX) | ||||
fatal("%s: too many PAM enviornment strings", __func__); | fatal("%s: too many PAM environment strings", __func__); | ||||
} | } | ||||
if ((r = sshbuf_put_u32(buffer, i)) != 0) | if ((r = sshbuf_put_u32(buffer, i)) != 0) | ||||
fatal("%s: buffer error: %s", __func__, ssh_err(r)); | fatal("%s: buffer error: %s", __func__, ssh_err(r)); | ||||
for (i = 0; env_from_pam != NULL && env_from_pam[i] != NULL; i++) { | for (i = 0; env_from_pam != NULL && env_from_pam[i] != NULL; i++) { | ||||
if ((r = sshbuf_put_cstring(buffer, env_from_pam[i])) != 0) | if ((r = sshbuf_put_cstring(buffer, env_from_pam[i])) != 0) | ||||
fatal("%s: buffer error: %s", __func__, ssh_err(r)); | fatal("%s: buffer error: %s", __func__, ssh_err(r)); | ||||
} | } | ||||
#endif /* UNSUPPORTED_POSIX_THREADS_HACK */ | #endif /* UNSUPPORTED_POSIX_THREADS_HACK */ | ||||
▲ Show 20 Lines • Show All 205 Lines • ▼ Show 20 Lines | expose_authinfo(const char *caller) | ||||
do_pam_putenv("SSH_AUTH_INFO_0", auth_info); | do_pam_putenv("SSH_AUTH_INFO_0", auth_info); | ||||
free(auth_info); | free(auth_info); | ||||
} | } | ||||
static void * | static void * | ||||
sshpam_init_ctx(Authctxt *authctxt) | sshpam_init_ctx(Authctxt *authctxt) | ||||
{ | { | ||||
struct pam_ctxt *ctxt; | struct pam_ctxt *ctxt; | ||||
int socks[2]; | int result, socks[2]; | ||||
debug3("PAM: %s entering", __func__); | debug3("PAM: %s entering", __func__); | ||||
/* | /* | ||||
* Refuse to start if we don't have PAM enabled or do_pam_account | * Refuse to start if we don't have PAM enabled or do_pam_account | ||||
* has previously failed. | * has previously failed. | ||||
*/ | */ | ||||
if (!options.use_pam || sshpam_account_status == 0) | if (!options.use_pam || sshpam_account_status == 0) | ||||
return NULL; | return NULL; | ||||
Show All 10 Lines | sshpam_init_ctx(Authctxt *authctxt) | ||||
/* Start the authentication thread */ | /* Start the authentication thread */ | ||||
if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, socks) == -1) { | if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, socks) == -1) { | ||||
error("PAM: failed create sockets: %s", strerror(errno)); | error("PAM: failed create sockets: %s", strerror(errno)); | ||||
free(ctxt); | free(ctxt); | ||||
return (NULL); | return (NULL); | ||||
} | } | ||||
ctxt->pam_psock = socks[0]; | ctxt->pam_psock = socks[0]; | ||||
ctxt->pam_csock = socks[1]; | ctxt->pam_csock = socks[1]; | ||||
if (pthread_create(&ctxt->pam_thread, NULL, sshpam_thread, ctxt) == -1) { | result = pthread_create(&ctxt->pam_thread, NULL, sshpam_thread, ctxt); | ||||
if (result != 0) { | |||||
error("PAM: failed to start authentication thread: %s", | error("PAM: failed to start authentication thread: %s", | ||||
strerror(errno)); | strerror(result)); | ||||
close(socks[0]); | close(socks[0]); | ||||
close(socks[1]); | close(socks[1]); | ||||
free(ctxt); | free(ctxt); | ||||
return (NULL); | return (NULL); | ||||
} | } | ||||
cleanup_ctxt = ctxt; | cleanup_ctxt = ctxt; | ||||
return (ctxt); | return (ctxt); | ||||
} | } | ||||
Show All 28 Lines | while (ssh_msg_recv(ctxt->pam_psock, buffer) == 0) { | ||||
case PAM_PROMPT_ECHO_OFF: | case PAM_PROMPT_ECHO_OFF: | ||||
*num = 1; | *num = 1; | ||||
len = plen + mlen + 1; | len = plen + mlen + 1; | ||||
**prompts = xreallocarray(**prompts, 1, len); | **prompts = xreallocarray(**prompts, 1, len); | ||||
strlcpy(**prompts + plen, msg, len - plen); | strlcpy(**prompts + plen, msg, len - plen); | ||||
plen += mlen; | plen += mlen; | ||||
**echo_on = (type == PAM_PROMPT_ECHO_ON); | **echo_on = (type == PAM_PROMPT_ECHO_ON); | ||||
free(msg); | free(msg); | ||||
sshbuf_free(buffer); | |||||
return (0); | return (0); | ||||
case PAM_ERROR_MSG: | case PAM_ERROR_MSG: | ||||
case PAM_TEXT_INFO: | case PAM_TEXT_INFO: | ||||
/* accumulate messages */ | /* accumulate messages */ | ||||
len = plen + mlen + 2; | len = plen + mlen + 2; | ||||
**prompts = xreallocarray(**prompts, 1, len); | **prompts = xreallocarray(**prompts, 1, len); | ||||
strlcpy(**prompts + plen, msg, len - plen); | strlcpy(**prompts + plen, msg, len - plen); | ||||
plen += mlen; | plen += mlen; | ||||
Show All 12 Lines | case PAM_AUTH_ERR: | ||||
debug3("PAM: %s", pam_strerror(sshpam_handle, type)); | debug3("PAM: %s", pam_strerror(sshpam_handle, type)); | ||||
if (**prompts != NULL && strlen(**prompts) != 0) { | if (**prompts != NULL && strlen(**prompts) != 0) { | ||||
*info = **prompts; | *info = **prompts; | ||||
**prompts = NULL; | **prompts = NULL; | ||||
*num = 0; | *num = 0; | ||||
**echo_on = 0; | **echo_on = 0; | ||||
ctxt->pam_done = -1; | ctxt->pam_done = -1; | ||||
free(msg); | free(msg); | ||||
sshbuf_free(buffer); | |||||
return 0; | return 0; | ||||
} | } | ||||
/* FALLTHROUGH */ | /* FALLTHROUGH */ | ||||
case PAM_SUCCESS: | case PAM_SUCCESS: | ||||
if (**prompts != NULL) { | if (**prompts != NULL) { | ||||
/* drain any accumulated messages */ | /* drain any accumulated messages */ | ||||
debug("PAM: %s", **prompts); | debug("PAM: %s", **prompts); | ||||
if ((r = sshbuf_put(loginmsg, **prompts, | if ((r = sshbuf_put(loginmsg, **prompts, | ||||
Show All 10 Lines | case PAM_SUCCESS: | ||||
fatal("Internal error: PAM auth " | fatal("Internal error: PAM auth " | ||||
"succeeded when it should have " | "succeeded when it should have " | ||||
"failed"); | "failed"); | ||||
import_environments(buffer); | import_environments(buffer); | ||||
*num = 0; | *num = 0; | ||||
**echo_on = 0; | **echo_on = 0; | ||||
ctxt->pam_done = 1; | ctxt->pam_done = 1; | ||||
free(msg); | free(msg); | ||||
sshbuf_free(buffer); | |||||
return (0); | return (0); | ||||
} | } | ||||
error("PAM: %s for %s%.100s from %.100s", msg, | error("PAM: %s for %s%.100s from %.100s", msg, | ||||
sshpam_authctxt->valid ? "" : "illegal user ", | sshpam_authctxt->valid ? "" : "illegal user ", | ||||
sshpam_authctxt->user, sshpam_rhost); | sshpam_authctxt->user, sshpam_rhost); | ||||
/* FALLTHROUGH */ | /* FALLTHROUGH */ | ||||
default: | default: | ||||
*num = 0; | *num = 0; | ||||
**echo_on = 0; | **echo_on = 0; | ||||
free(msg); | free(msg); | ||||
ctxt->pam_done = -1; | ctxt->pam_done = -1; | ||||
sshbuf_free(buffer); | |||||
return (-1); | return (-1); | ||||
} | } | ||||
} | } | ||||
sshbuf_free(buffer); | |||||
return (-1); | return (-1); | ||||
} | } | ||||
/* | /* | ||||
* Returns a junk password of identical length to that the user supplied. | * Returns a junk password of identical length to that the user supplied. | ||||
* Used to mitigate timing attacks against crypt(3)/PAM stacks that | * Used to mitigate timing attacks against crypt(3)/PAM stacks that | ||||
* vary processing time in proportion to password length. | * vary processing time in proportion to password length. | ||||
*/ | */ | ||||
▲ Show 20 Lines • Show All 277 Lines • ▼ Show 20 Lines | |||||
* Set a PAM environment string. We need to do this so that the session | * Set a PAM environment string. We need to do this so that the session | ||||
* modules can handle things like Kerberos/GSI credentials that appear | * modules can handle things like Kerberos/GSI credentials that appear | ||||
* during the ssh authentication process. | * during the ssh authentication process. | ||||
*/ | */ | ||||
int | int | ||||
do_pam_putenv(char *name, char *value) | do_pam_putenv(char *name, char *value) | ||||
{ | { | ||||
int ret = 1; | int ret = 1; | ||||
#ifdef HAVE_PAM_PUTENV | |||||
char *compound; | char *compound; | ||||
size_t len; | size_t len; | ||||
len = strlen(name) + strlen(value) + 2; | len = strlen(name) + strlen(value) + 2; | ||||
compound = xmalloc(len); | compound = xmalloc(len); | ||||
snprintf(compound, len, "%s=%s", name, value); | snprintf(compound, len, "%s=%s", name, value); | ||||
ret = pam_putenv(sshpam_handle, compound); | ret = pam_putenv(sshpam_handle, compound); | ||||
free(compound); | free(compound); | ||||
#endif | |||||
return (ret); | return (ret); | ||||
} | } | ||||
char ** | char ** | ||||
fetch_pam_child_environment(void) | fetch_pam_child_environment(void) | ||||
{ | { | ||||
return sshpam_env; | return sshpam_env; | ||||
▲ Show 20 Lines • Show All 149 Lines • Show Last 20 Lines |