From d41ac64ad68811d8575db6e456fba470b0c3fde7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Bobbio?= Date: Sat, 23 Apr 2011 02:35:02 +0200 Subject: [PATCH 1/8] Add UnixSocketsGroupWritable config flag When running a system-wide instance of Tor on Unix-like systems, having a ControlSocket is a quite handy mechanism to access Tor control channel. But it would be easier if access to the Unix domain socket can be granted by making control users members of the group running the Tor process. This change introduces a UnixSocketsGroupWritable option, which will create Unix domain sockets (and thus ControlSocket) 'g+rw'. This allows ControlSocket to offer same access control measures than ControlPort+CookieAuthFileGroupReadable. See for more details. --- doc/tor.1.txt | 5 +++++ src/or/config.c | 3 ++- src/or/connection.c | 7 +++++++ src/or/or.h | 1 + 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/doc/tor.1.txt b/doc/tor.1.txt index d95d764c67..d0d0c2f7cb 100644 --- a/doc/tor.1.txt +++ b/doc/tor.1.txt @@ -167,6 +167,11 @@ Other options can be specified either on the command-line (--option Like ControlPort, but listens on a Unix domain socket, rather than a TCP socket. (Unix and Unix-like systems only.) +**UnixSocketsGroupWritable** **0**|**1**:: + If this option is set to 0, don't allow the filesystem group to read and + write unix sockets (e.g. ControlSocket). If the option is set to 1, make + the control socket readable and writable by the default GID. (Default: 0) + **HashedControlPassword** __hashed_password__:: Don't allow any connections on the control port except when the other process knows the password whose one-way hash is __hashed_password__. You diff --git a/src/or/config.c b/src/or/config.c index 6a2742d954..c81fc9c594 100644 --- a/src/or/config.c +++ b/src/or/config.c @@ -209,6 +209,7 @@ static config_var_t _option_vars[] = { V(ControlPortFileGroupReadable,BOOL, "0"), V(ControlPortWriteToFile, FILENAME, NULL), V(ControlSocket, LINELIST, NULL), + V(UnixSocketsGroupWritable, BOOL, "0"), V(CookieAuthentication, BOOL, "0"), V(CookieAuthFileGroupReadable, BOOL, "0"), V(CookieAuthFile, STRING, NULL), @@ -952,7 +953,7 @@ options_act_reversible(or_options_t *old_options, char **msg) } #ifndef HAVE_SYS_UN_H - if (options->ControlSocket) { + if (options->ControlSocket || options->UnixSocketsGroupWritable) { *msg = tor_strdup("Unix domain sockets (ControlSocket) not supported" " on this OS/with this build."); goto rollback; diff --git a/src/or/connection.c b/src/or/connection.c index 01b533d9b5..d0898c5e5c 100644 --- a/src/or/connection.c +++ b/src/or/connection.c @@ -966,6 +966,13 @@ connection_create_listener(const struct sockaddr *listensockaddr, tor_socket_strerror(tor_socket_errno(s))); goto err; } + if (get_options()->UnixSocketsGroupWritable) { + if (chmod(address, 0660) < 0) { + log_warn(LD_FS,"Unable to make %s group-readable.", address); + tor_close_socket(s); + goto err; + } + } if (listen(s,SOMAXCONN) < 0) { log_warn(LD_NET, "Could not listen on %s: %s", address, diff --git a/src/or/or.h b/src/or/or.h index 5647691550..b72693f029 100644 --- a/src/or/or.h +++ b/src/or/or.h @@ -2445,6 +2445,7 @@ typedef struct { int ControlPort; /**< Port to listen on for control connections. */ config_line_t *ControlSocket; /**< List of Unix Domain Sockets to listen on * for control connections. */ + int UnixSocketsGroupWritable; /**< Boolean: Are unix sockets g+rw? */ int DirPort; /**< Port to listen on for directory connections. */ int DNSPort; /**< Port to listen on for DNS requests. */ int AssumeReachable; /**< Whether to publish our descriptor regardless. */ From 4198261291c8edbd5ba1617b7bfe3563e51edbe7 Mon Sep 17 00:00:00 2001 From: Sebastian Hahn Date: Sat, 23 Apr 2011 02:57:53 +0200 Subject: [PATCH 2/8] Clean up the 2972 implementation a little --- changes/bug2972 | 5 +++++ doc/tor.1.txt | 2 +- src/or/config.c | 14 ++++++++++---- src/or/connection.c | 4 ++-- src/or/or.h | 2 +- 5 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 changes/bug2972 diff --git a/changes/bug2972 b/changes/bug2972 new file mode 100644 index 0000000000..26afcca421 --- /dev/null +++ b/changes/bug2972 @@ -0,0 +1,5 @@ + o Minor features: + - Allow ControlSockets to be group-writable when the + ControlSocksGroupWritable configuration option is turned on. Patch + by Jérémy Bobbio; implements ticket 2972. + diff --git a/doc/tor.1.txt b/doc/tor.1.txt index d0d0c2f7cb..1815a8d963 100644 --- a/doc/tor.1.txt +++ b/doc/tor.1.txt @@ -167,7 +167,7 @@ Other options can be specified either on the command-line (--option Like ControlPort, but listens on a Unix domain socket, rather than a TCP socket. (Unix and Unix-like systems only.) -**UnixSocketsGroupWritable** **0**|**1**:: +**ControlSocketsGroupWritable** **0**|**1**:: If this option is set to 0, don't allow the filesystem group to read and write unix sockets (e.g. ControlSocket). If the option is set to 1, make the control socket readable and writable by the default GID. (Default: 0) diff --git a/src/or/config.c b/src/or/config.c index c81fc9c594..614fc48c3e 100644 --- a/src/or/config.c +++ b/src/or/config.c @@ -209,7 +209,7 @@ static config_var_t _option_vars[] = { V(ControlPortFileGroupReadable,BOOL, "0"), V(ControlPortWriteToFile, FILENAME, NULL), V(ControlSocket, LINELIST, NULL), - V(UnixSocketsGroupWritable, BOOL, "0"), + V(ControlSocketsGroupWritable, BOOL, "0"), V(CookieAuthentication, BOOL, "0"), V(CookieAuthFileGroupReadable, BOOL, "0"), V(CookieAuthFile, STRING, NULL), @@ -953,9 +953,15 @@ options_act_reversible(or_options_t *old_options, char **msg) } #ifndef HAVE_SYS_UN_H - if (options->ControlSocket || options->UnixSocketsGroupWritable) { - *msg = tor_strdup("Unix domain sockets (ControlSocket) not supported" - " on this OS/with this build."); + if (options->ControlSocket || options->ControlSocketsGroupWritable) { + *msg = tor_strdup("Unix domain sockets (ControlSocket) not supported " + "on this OS/with this build."); + goto rollback; + } +#else + if (options->ControlSocketsGroupWritable && !options->ControlSocket) { + *msg = tor_strdup("Setting ControlSocketGroupWritable without setting" + "a ControlSocket makes no sense."); goto rollback; } #endif diff --git a/src/or/connection.c b/src/or/connection.c index d0898c5e5c..12e00e59bb 100644 --- a/src/or/connection.c +++ b/src/or/connection.c @@ -966,9 +966,9 @@ connection_create_listener(const struct sockaddr *listensockaddr, tor_socket_strerror(tor_socket_errno(s))); goto err; } - if (get_options()->UnixSocketsGroupWritable) { + if (get_options()->ControlSocketsGroupWritable) { if (chmod(address, 0660) < 0) { - log_warn(LD_FS,"Unable to make %s group-readable.", address); + log_warn(LD_FS,"Unable to make %s group-writable.", address); tor_close_socket(s); goto err; } diff --git a/src/or/or.h b/src/or/or.h index b72693f029..b9d8319ba5 100644 --- a/src/or/or.h +++ b/src/or/or.h @@ -2445,7 +2445,7 @@ typedef struct { int ControlPort; /**< Port to listen on for control connections. */ config_line_t *ControlSocket; /**< List of Unix Domain Sockets to listen on * for control connections. */ - int UnixSocketsGroupWritable; /**< Boolean: Are unix sockets g+rw? */ + int ControlSocketsGroupWritable; /**< Boolean: Are control sockets g+rw? */ int DirPort; /**< Port to listen on for directory connections. */ int DNSPort; /**< Port to listen on for DNS requests. */ int AssumeReachable; /**< Whether to publish our descriptor regardless. */ From b147c01295deb8c09807fda3b5e4945edcfaf81a Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Fri, 13 May 2011 14:55:40 -0400 Subject: [PATCH 3/8] Make check_private_dir accept g+rx dirs if told to do so. --- src/common/util.c | 33 ++++++++++++++++++++++++--------- src/common/util.h | 6 +++++- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/common/util.c b/src/common/util.c index 86f4141674..3f81874331 100644 --- a/src/common/util.c +++ b/src/common/util.c @@ -1665,16 +1665,22 @@ file_status(const char *fname) } /** Check whether dirname exists and is private. If yes return 0. If - * it does not exist, and check==CPD_CREATE is set, try to create it + * it does not exist, and check&CPD_CREATE is set, try to create it * and return 0 on success. If it does not exist, and - * check==CPD_CHECK, and we think we can create it, return 0. Else - * return -1. */ + * check&CPD_CHECK, and we think we can create it, return 0. Else + * return -1. If CPD_GROUP_OK is set, then it's okay if the directory + * is group-readable, but in all cases we create the directory mode 0700. + */ int check_private_dir(const char *dirname, cpd_check_t check) { int r; struct stat st; char *f; +#ifndef MS_WINDOWS + int mask; +#endif + tor_assert(dirname); f = tor_strdup(dirname); clean_name_for_stat(f); @@ -1686,10 +1692,7 @@ check_private_dir(const char *dirname, cpd_check_t check) strerror(errno)); return -1; } - if (check == CPD_NONE) { - log_warn(LD_FS, "Directory %s does not exist.", dirname); - return -1; - } else if (check == CPD_CREATE) { + if (check & CPD_CREATE) { log_info(LD_GENERAL, "Creating directory %s", dirname); #if defined (MS_WINDOWS) && !defined (WINCE) r = mkdir(dirname); @@ -1701,6 +1704,9 @@ check_private_dir(const char *dirname, cpd_check_t check) strerror(errno)); return -1; } + } else if (!(check & CPD_CHECK)) { + log_warn(LD_FS, "Directory %s does not exist.", dirname); + return -1; } /* XXXX In the case where check==CPD_CHECK, we should look at the * parent directory a little harder. */ @@ -1728,9 +1734,18 @@ check_private_dir(const char *dirname, cpd_check_t check) tor_free(process_ownername); return -1; } - if (st.st_mode & 0077) { + if (check & CPD_GROUP_OK) { + mask = 0027; + } else { + mask = 0077; + } + if (st.st_mode & mask) { + unsigned new_mode; log_warn(LD_FS, "Fixing permissions on directory %s", dirname); - if (chmod(dirname, 0700)) { + new_mode = st.st_mode; + new_mode |= 0700; /* Owner should have rwx */ + new_mode &= ~mask; /* Clear the other bits that we didn't want set...*/ + if (chmod(dirname, new_mode)) { log_warn(LD_FS, "Could not chmod directory %s: %s", dirname, strerror(errno)); return -1; diff --git a/src/common/util.h b/src/common/util.h index 961b5875ad..f75953226b 100644 --- a/src/common/util.h +++ b/src/common/util.h @@ -286,7 +286,11 @@ file_status_t file_status(const char *filename); /** Possible behaviors for check_private_dir() on encountering a nonexistent * directory; see that function's documentation for details. */ -typedef enum { CPD_NONE, CPD_CREATE, CPD_CHECK } cpd_check_t; +typedef unsigned int cpd_check_t; +#define CPD_NONE 0 +#define CPD_CREATE 1 +#define CPD_CHECK 2 +#define CPD_GROUP_OK 4 int check_private_dir(const char *dirname, cpd_check_t check); #define OPEN_FLAGS_REPLACE (O_WRONLY|O_CREAT|O_TRUNC) #define OPEN_FLAGS_APPEND (O_WRONLY|O_CREAT|O_APPEND) From 3b6cbf253494303f612eeb09a6fbb30a7c15c7fa Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Fri, 13 May 2011 15:15:41 -0400 Subject: [PATCH 4/8] Add a function to pull off the final component of a path --- src/common/compat.c | 34 ++++++++++++++++++++++++++++++++++ src/common/compat.h | 2 ++ src/or/connection.c | 2 ++ src/test/test_util.c | 30 ++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+) diff --git a/src/common/compat.c b/src/common/compat.c index ea7f9d7efc..3c5e9385e2 100644 --- a/src/common/compat.c +++ b/src/common/compat.c @@ -1467,6 +1467,40 @@ get_user_homedir(const char *username) } #endif +/** Modify fname to contain the name of the directory */ +int +get_parent_directory(char *fname) +{ + char *cp; + int at_end = 1; + tor_assert(fname); +#ifdef MS_WINDOWS + /* If we start with, say, c:, then don't consider that the start of the path + */ + if (fname[0] && fname[1] == ':') { + fname += 2; + } +#endif + /* Now we want to remove the final character that */ + cp = fname + strlen(fname); + at_end = 1; + while (--cp > fname) { + int is_sep = (*cp == '/' +#ifdef MS_WINDOWS + || *cp == '\\' +#endif + ); + if (is_sep) { + *cp = '\0'; + if (! at_end) + return 0; + } else { + at_end = 0; + } + } + return -1; +} + /** Set *addr to the IP address (in dotted-quad notation) stored in c. * Return 1 on success, 0 if c is badly formatted. (Like inet_aton(c,addr), * but works on Windows and Solaris.) diff --git a/src/common/compat.h b/src/common/compat.h index af795ffba9..eff51ab30c 100644 --- a/src/common/compat.h +++ b/src/common/compat.h @@ -552,6 +552,8 @@ int switch_id(const char *user); char *get_user_homedir(const char *username); #endif +int get_parent_directory(char *fname); + int spawn_func(void (*func)(void *), void *data); void spawn_exit(void) ATTR_NORETURN; diff --git a/src/or/connection.c b/src/or/connection.c index 12e00e59bb..ab265df1af 100644 --- a/src/or/connection.c +++ b/src/or/connection.c @@ -967,6 +967,8 @@ connection_create_listener(const struct sockaddr *listensockaddr, goto err; } if (get_options()->ControlSocketsGroupWritable) { + /* We need to use chmod; fchmod doesn't work on sockets on all + * platforms. */ if (chmod(address, 0660) < 0) { log_warn(LD_FS,"Unable to make %s group-writable.", address); tor_close_socket(s); diff --git a/src/test/test_util.c b/src/test/test_util.c index 0da45df499..23cd059cf7 100644 --- a/src/test/test_util.c +++ b/src/test/test_util.c @@ -1195,6 +1195,35 @@ test_util_listdir(void *ptr) } } +static void +test_util_parent_dir(void *ptr) +{ + char *cp; + (void)ptr; + +#define T(input,expect_ok,output) \ + do { \ + int ok; \ + cp = tor_strdup(input); \ + ok = get_parent_directory(cp); \ + tt_int_op(ok, ==, expect_ok); \ + if (ok==0) \ + tt_str_op(cp, ==, output); \ + tor_free(cp); \ + } while (0); + + T("/home/wombat/knish", 0, "/home/wombat"); + T("/home/wombat/knish/", 0, "/home/wombat"); + T("./home/wombat/knish/", 0, "./home/wombat"); + T("./wombat", 0, "."); + T("", -1, ""); + T("/", -1, ""); + T("////", -1, ""); + + done: + tor_free(cp); +} + #ifdef MS_WINDOWS static void test_util_load_win_lib(void *ptr) @@ -1286,6 +1315,7 @@ struct testcase_t util_tests[] = { UTIL_TEST(find_str_at_start_of_line, 0), UTIL_TEST(asprintf, 0), UTIL_TEST(listdir, 0), + UTIL_TEST(parent_dir, 0), #ifdef MS_WINDOWS UTIL_TEST(load_win_lib, 0), #endif From 5d147d8527da3c8cff7f5ab5f0d0185d51fff79b Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Fri, 13 May 2011 15:40:03 -0400 Subject: [PATCH 5/8] Add a new flag to check_private_dir to make it _not_ change permissions We'll need this for checking permissions on the directories that hold control sockets: if somebody says "ControlSocket ~/foo", it would be pretty rude to do a chmod 700 on their homedir. --- src/common/util.c | 7 +++++++ src/common/util.h | 1 + 2 files changed, 8 insertions(+) diff --git a/src/common/util.c b/src/common/util.c index 3f81874331..d84ed9c00e 100644 --- a/src/common/util.c +++ b/src/common/util.c @@ -1670,6 +1670,8 @@ file_status(const char *fname) * check&CPD_CHECK, and we think we can create it, return 0. Else * return -1. If CPD_GROUP_OK is set, then it's okay if the directory * is group-readable, but in all cases we create the directory mode 0700. + * If CPD_CHECK_MODE_ONLY is set, then we don't alter the directory permissions + * if they are too permissive: we just return -1. */ int check_private_dir(const char *dirname, cpd_check_t check) @@ -1741,6 +1743,11 @@ check_private_dir(const char *dirname, cpd_check_t check) } if (st.st_mode & mask) { unsigned new_mode; + if (check & CPD_CHECK_MODE_ONLY) { + log_warn(LD_FS, "Permissions on directory %s are too permissive.", + dirname); + return -1; + } log_warn(LD_FS, "Fixing permissions on directory %s", dirname); new_mode = st.st_mode; new_mode |= 0700; /* Owner should have rwx */ diff --git a/src/common/util.h b/src/common/util.h index f75953226b..f32709accd 100644 --- a/src/common/util.h +++ b/src/common/util.h @@ -291,6 +291,7 @@ typedef unsigned int cpd_check_t; #define CPD_CREATE 1 #define CPD_CHECK 2 #define CPD_GROUP_OK 4 +#define CPD_CHECK_MODE_ONLY 8 int check_private_dir(const char *dirname, cpd_check_t check); #define OPEN_FLAGS_REPLACE (O_WRONLY|O_CREAT|O_TRUNC) #define OPEN_FLAGS_APPEND (O_WRONLY|O_CREAT|O_APPEND) From 4b800408fa85ce0ac81a308c42d654b3357180d4 Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Fri, 13 May 2011 15:46:53 -0400 Subject: [PATCH 6/8] Check permissions on the directory holding a control socket --- changes/bug2792_checkdir | 8 ++++++++ src/or/connection.c | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 changes/bug2792_checkdir diff --git a/changes/bug2792_checkdir b/changes/bug2792_checkdir new file mode 100644 index 0000000000..10de1deb2d --- /dev/null +++ b/changes/bug2792_checkdir @@ -0,0 +1,8 @@ + o Minor features: + - Tor now refuses to create a ControlSocket in a directory that is + world-readable (or group-readable if ControlSocketsGroupWritable + is 0). This is necessary because some operating systems do not + check the permissions on an AF_UNIX socket when programs try to + connect to it. Checking permissions on the directory holding + the socket, however, seems to work everywhere. + diff --git a/src/or/connection.c b/src/or/connection.c index ab265df1af..b7d6fe408d 100644 --- a/src/or/connection.c +++ b/src/or/connection.c @@ -853,6 +853,43 @@ warn_too_many_conns(void) } } +#ifdef HAVE_SYS_UN_H +/** Check whether we should be willing to open an AF_UNIX socket in + * path. Return 0 if we should go ahead and -1 if we shouldn't. */ +static int +check_location_for_unix_socket(or_options_t *options, const char *path) +{ + int r = -1; + char *p = tor_strdup(path); + cpd_check_t flags = CPD_CHECK_MODE_ONLY; + if (get_parent_directory(p)<0) + goto done; + + if (options->ControlSocketsGroupWritable) + flags |= CPD_GROUP_OK; + + if (check_private_dir(p, flags) < 0) { + char *escpath, *escdir; + escpath = esc_for_log(path); + escdir = esc_for_log(p); + log_warn(LD_GENERAL, "Before Tor can create a control socket in %s, the " + "directory %s needs to exist, and to be accessible only by the " + "user%s account that is running Tor. (On some Unix systems, " + "anybody who can list a socket can conect to it, so Tor is " + "being careful.)", escpath, escdir, + options->ControlSocketsGroupWritable ? " and group" : ""); + tor_free(escpath); + tor_free(escdir); + goto done; + } + + r = 0; + done: + tor_free(p); + return r; +} +#endif + /** Bind a new non-blocking socket listening to the socket described * by listensockaddr. * @@ -947,6 +984,9 @@ connection_create_listener(const struct sockaddr *listensockaddr, * and listeners at the same time */ tor_assert(type == CONN_TYPE_CONTROL_LISTENER); + if (check_location_for_unix_socket(get_options(), address) < 0) + goto err; + log_notice(LD_NET, "Opening %s on %s", conn_type_to_string(type), address); From 287f6cb128c890e31faa951be6d42cd6801f4e59 Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Sun, 15 May 2011 13:03:15 -0400 Subject: [PATCH 7/8] Fix up some comment issues spotted by rransom --- src/common/compat.c | 7 ++++++- src/common/util.c | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/common/compat.c b/src/common/compat.c index 3c5e9385e2..fc066da681 100644 --- a/src/common/compat.c +++ b/src/common/compat.c @@ -1481,7 +1481,12 @@ get_parent_directory(char *fname) fname += 2; } #endif - /* Now we want to remove the final character that */ + /* Now we want to remove all path-separators at the end of the string, + * and to remove the end of the string starting with the path separator + * before the last non-path-separator. In perl, this would be + * s#[/]*$##; s#/[^/]*$##; + * on a unixy platform. + */ cp = fname + strlen(fname); at_end = 1; while (--cp > fname) { diff --git a/src/common/util.c b/src/common/util.c index d84ed9c00e..0e739f2127 100644 --- a/src/common/util.c +++ b/src/common/util.c @@ -1664,10 +1664,10 @@ file_status(const char *fname) return FN_ERROR; } -/** Check whether dirname exists and is private. If yes return 0. If - * it does not exist, and check&CPD_CREATE is set, try to create it +/** Check whether dirname exists and is private. If yes return 0. If + * it does not exist, and check&CPD_CREATE is set, try to create it * and return 0 on success. If it does not exist, and - * check&CPD_CHECK, and we think we can create it, return 0. Else + * check&CPD_CHECK, and we think we can create it, return 0. Else * return -1. If CPD_GROUP_OK is set, then it's okay if the directory * is group-readable, but in all cases we create the directory mode 0700. * If CPD_CHECK_MODE_ONLY is set, then we don't alter the directory permissions From f72e792be5437c9ee11d3f498ed3bb469b46d1bb Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Sun, 15 May 2011 13:11:48 -0400 Subject: [PATCH 8/8] Make check_private_dir check for group ownership as appropriate --- src/common/util.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/common/util.c b/src/common/util.c index 0e739f2127..1bb116b212 100644 --- a/src/common/util.c +++ b/src/common/util.c @@ -30,6 +30,7 @@ #else #include #include +#include #endif /* math.h needs this on Linux */ @@ -1736,6 +1737,21 @@ check_private_dir(const char *dirname, cpd_check_t check) tor_free(process_ownername); return -1; } + if ((check & CPD_GROUP_OK) && st.st_gid != getgid()) { + struct group *gr; + char *process_groupname = NULL; + gr = getgrgid(getgid()); + process_groupname = gr ? tor_strdup(gr->gr_name) : tor_strdup(""); + gr = getgrgid(st.st_gid); + + log_warn(LD_FS, "%s is not owned by this group (%s, %d) but by group " + "%s (%d). Are you running Tor as the wrong user?", + dirname, process_groupname, (int)getgid(), + gr ? gr->gr_name : "", (int)st.st_gid); + + tor_free(process_groupname); + return -1; + } if (check & CPD_GROUP_OK) { mask = 0027; } else {