2017-11-18 17:09:20 +01:00
|
|
|
/* SPDX-License-Identifier: LGPL-2.1+ */
|
2012-09-06 22:23:11 +02:00
|
|
|
|
|
|
|
#include "selinux-access.h"
|
|
|
|
|
2017-10-03 10:41:51 +02:00
|
|
|
#if HAVE_SELINUX
|
2012-09-06 22:23:11 +02:00
|
|
|
|
|
|
|
#include <errno.h>
|
|
|
|
#include <selinux/avc.h>
|
2015-11-16 22:09:36 +01:00
|
|
|
#include <selinux/selinux.h>
|
2017-10-03 10:41:51 +02:00
|
|
|
#if HAVE_AUDIT
|
2012-09-06 22:23:11 +02:00
|
|
|
#include <libaudit.h>
|
|
|
|
#endif
|
2012-10-02 23:56:54 +02:00
|
|
|
|
2013-11-19 21:12:59 +01:00
|
|
|
#include "sd-bus.h"
|
2015-10-27 01:26:31 +01:00
|
|
|
|
2015-10-27 03:01:06 +01:00
|
|
|
#include "alloc-util.h"
|
2015-10-27 01:26:31 +01:00
|
|
|
#include "audit-fd.h"
|
2013-11-19 21:12:59 +01:00
|
|
|
#include "bus-util.h"
|
2019-07-03 16:56:17 +02:00
|
|
|
#include "errno-util.h"
|
2019-03-27 11:32:41 +01:00
|
|
|
#include "format-util.h"
|
2012-10-02 23:56:54 +02:00
|
|
|
#include "log.h"
|
2015-10-27 01:26:31 +01:00
|
|
|
#include "path-util.h"
|
2012-10-02 23:56:54 +02:00
|
|
|
#include "selinux-util.h"
|
2015-10-27 01:26:31 +01:00
|
|
|
#include "stdio-util.h"
|
2013-11-28 17:50:02 +01:00
|
|
|
#include "strv.h"
|
2015-10-27 01:26:31 +01:00
|
|
|
#include "util.h"
|
2012-09-06 22:23:11 +02:00
|
|
|
|
2012-10-02 23:07:00 +02:00
|
|
|
static bool initialized = false;
|
2012-09-06 22:23:11 +02:00
|
|
|
|
2013-11-28 17:50:02 +01:00
|
|
|
struct audit_info {
|
|
|
|
sd_bus_creds *creds;
|
2012-09-06 22:23:11 +02:00
|
|
|
const char *path;
|
2013-11-28 17:50:02 +01:00
|
|
|
const char *cmdline;
|
2012-09-06 22:23:11 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Any time an access gets denied this callback will be called
|
2014-10-28 02:31:29 +01:00
|
|
|
with the audit data. We then need to just copy the audit data into the msgbuf.
|
2012-09-06 22:23:11 +02:00
|
|
|
*/
|
2012-10-02 23:07:00 +02:00
|
|
|
static int audit_callback(
|
|
|
|
void *auditdata,
|
|
|
|
security_class_t cls,
|
|
|
|
char *msgbuf,
|
|
|
|
size_t msgbufsize) {
|
|
|
|
|
2013-11-28 17:50:02 +01:00
|
|
|
const struct audit_info *audit = auditdata;
|
|
|
|
uid_t uid = 0, login_uid = 0;
|
|
|
|
gid_t gid = 0;
|
2015-01-27 14:00:11 +01:00
|
|
|
char login_uid_buf[DECIMAL_STR_MAX(uid_t) + 1] = "n/a";
|
|
|
|
char uid_buf[DECIMAL_STR_MAX(uid_t) + 1] = "n/a";
|
|
|
|
char gid_buf[DECIMAL_STR_MAX(gid_t) + 1] = "n/a";
|
2013-11-28 17:50:02 +01:00
|
|
|
|
2014-10-28 02:31:29 +01:00
|
|
|
if (sd_bus_creds_get_audit_login_uid(audit->creds, &login_uid) >= 0)
|
2015-01-27 14:00:11 +01:00
|
|
|
xsprintf(login_uid_buf, UID_FMT, login_uid);
|
bus: use EUID over UID and fix unix-creds
Whenever a process performs an action on an object, the kernel uses the
EUID of the process to do permission checks and to apply on any newly
created objects. The UID of a process is only used if someone *ELSE* acts
on the process. That is, the UID of a process defines who owns the
process, the EUID defines what privileges are used by this process when
performing an action.
Process limits, on the other hand, are always applied to the real UID, not
the effective UID. This is, because a process has a user object linked,
which always corresponds to its UID. A process never has a user object
linked for its EUID. Thus, accounting (and limits) is always done on the
real UID.
This commit fixes all sd-bus users to use the EUID when performing
privilege checks and alike. Furthermore, it fixes unix-creds to be parsed
as EUID, not UID (as the kernel always takes the EUID on UDS). Anyone
using UID (eg., to do user-accounting) has to fall back to the EUID as UDS
does not transmit the UID.
2015-01-18 13:55:55 +01:00
|
|
|
if (sd_bus_creds_get_euid(audit->creds, &uid) >= 0)
|
2015-01-27 14:00:11 +01:00
|
|
|
xsprintf(uid_buf, UID_FMT, uid);
|
bus: use EUID over UID and fix unix-creds
Whenever a process performs an action on an object, the kernel uses the
EUID of the process to do permission checks and to apply on any newly
created objects. The UID of a process is only used if someone *ELSE* acts
on the process. That is, the UID of a process defines who owns the
process, the EUID defines what privileges are used by this process when
performing an action.
Process limits, on the other hand, are always applied to the real UID, not
the effective UID. This is, because a process has a user object linked,
which always corresponds to its UID. A process never has a user object
linked for its EUID. Thus, accounting (and limits) is always done on the
real UID.
This commit fixes all sd-bus users to use the EUID when performing
privilege checks and alike. Furthermore, it fixes unix-creds to be parsed
as EUID, not UID (as the kernel always takes the EUID on UDS). Anyone
using UID (eg., to do user-accounting) has to fall back to the EUID as UDS
does not transmit the UID.
2015-01-18 13:55:55 +01:00
|
|
|
if (sd_bus_creds_get_egid(audit->creds, &gid) >= 0)
|
2015-01-27 14:00:11 +01:00
|
|
|
xsprintf(gid_buf, GID_FMT, gid);
|
2012-10-02 23:07:00 +02:00
|
|
|
|
2012-09-06 22:23:11 +02:00
|
|
|
snprintf(msgbuf, msgbufsize,
|
2014-10-28 02:31:29 +01:00
|
|
|
"auid=%s uid=%s gid=%s%s%s%s%s%s%s",
|
|
|
|
login_uid_buf, uid_buf, gid_buf,
|
2013-11-28 17:50:02 +01:00
|
|
|
audit->path ? " path=\"" : "", strempty(audit->path), audit->path ? "\"" : "",
|
|
|
|
audit->cmdline ? " cmdline=\"" : "", strempty(audit->cmdline), audit->cmdline ? "\"" : "");
|
2012-09-28 16:26:59 +02:00
|
|
|
|
2012-09-06 22:23:11 +02:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2015-02-24 02:06:00 +01:00
|
|
|
static int callback_type_to_priority(int type) {
|
|
|
|
switch(type) {
|
2015-06-12 13:35:59 +02:00
|
|
|
|
|
|
|
case SELINUX_ERROR:
|
|
|
|
return LOG_ERR;
|
|
|
|
|
|
|
|
case SELINUX_WARNING:
|
|
|
|
return LOG_WARNING;
|
|
|
|
|
|
|
|
case SELINUX_INFO:
|
|
|
|
return LOG_INFO;
|
|
|
|
|
2015-02-24 02:06:00 +01:00
|
|
|
case SELINUX_AVC:
|
2015-06-12 13:35:59 +02:00
|
|
|
default:
|
|
|
|
return LOG_NOTICE;
|
2015-02-24 02:06:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-09-06 22:23:11 +02:00
|
|
|
/*
|
2015-02-24 02:06:00 +01:00
|
|
|
libselinux uses this callback when access gets denied or other
|
|
|
|
events happen. If audit is turned on, messages will be reported
|
|
|
|
using audit netlink, otherwise they will be logged using the usual
|
|
|
|
channels.
|
|
|
|
|
|
|
|
Code copied from dbus and modified.
|
2012-09-06 22:23:11 +02:00
|
|
|
*/
|
2013-10-16 03:17:09 +02:00
|
|
|
_printf_(2, 3) static int log_callback(int type, const char *fmt, ...) {
|
2012-09-06 22:23:11 +02:00
|
|
|
va_list ap;
|
2016-04-09 17:13:26 +02:00
|
|
|
const char *fmt2;
|
2012-09-06 22:23:11 +02:00
|
|
|
|
2017-10-03 10:41:51 +02:00
|
|
|
#if HAVE_AUDIT
|
2015-02-24 02:06:00 +01:00
|
|
|
int fd;
|
|
|
|
|
|
|
|
fd = get_audit_fd();
|
|
|
|
|
|
|
|
if (fd >= 0) {
|
2013-02-13 19:35:08 +01:00
|
|
|
_cleanup_free_ char *buf = NULL;
|
|
|
|
int r;
|
2012-09-06 22:23:11 +02:00
|
|
|
|
2013-11-28 17:50:02 +01:00
|
|
|
va_start(ap, fmt);
|
2013-02-13 19:35:08 +01:00
|
|
|
r = vasprintf(&buf, fmt, ap);
|
2012-09-21 10:23:08 +02:00
|
|
|
va_end(ap);
|
2012-10-02 23:07:00 +02:00
|
|
|
|
2013-02-13 19:35:08 +01:00
|
|
|
if (r >= 0) {
|
2019-02-26 17:33:27 +01:00
|
|
|
if (type == SELINUX_AVC)
|
|
|
|
audit_log_user_avc_message(get_audit_fd(), AUDIT_USER_AVC, buf, NULL, NULL, NULL, 0);
|
|
|
|
else if (type == SELINUX_ERROR)
|
|
|
|
audit_log_user_avc_message(get_audit_fd(), AUDIT_USER_SELINUX_ERR, buf, NULL, NULL, NULL, 0);
|
|
|
|
|
2013-02-13 19:35:08 +01:00
|
|
|
return 0;
|
|
|
|
}
|
2012-09-06 22:23:11 +02:00
|
|
|
}
|
|
|
|
#endif
|
2013-11-28 17:50:02 +01:00
|
|
|
|
2016-04-09 17:13:26 +02:00
|
|
|
fmt2 = strjoina("selinux: ", fmt);
|
|
|
|
|
2013-11-28 17:50:02 +01:00
|
|
|
va_start(ap, fmt);
|
2020-05-25 18:16:33 +02:00
|
|
|
|
|
|
|
DISABLE_WARNING_FORMAT_NONLITERAL;
|
2017-04-20 20:09:47 +02:00
|
|
|
log_internalv(LOG_AUTH | callback_type_to_priority(type),
|
2019-06-07 15:00:47 +02:00
|
|
|
0, PROJECT_FILE, __LINE__, __FUNCTION__,
|
2017-04-20 20:09:47 +02:00
|
|
|
fmt2, ap);
|
2020-05-25 18:16:33 +02:00
|
|
|
REENABLE_WARNING;
|
2012-09-06 22:23:11 +02:00
|
|
|
va_end(ap);
|
2012-10-02 23:07:00 +02:00
|
|
|
|
2012-09-06 22:23:11 +02:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2015-11-30 20:26:03 +01:00
|
|
|
static int access_init(sd_bus_error *error) {
|
2012-09-06 22:23:11 +02:00
|
|
|
|
2015-11-30 20:26:03 +01:00
|
|
|
if (!mac_selinux_use())
|
|
|
|
return 0;
|
2012-09-06 22:23:11 +02:00
|
|
|
|
2015-11-30 20:26:03 +01:00
|
|
|
if (initialized)
|
|
|
|
return 1;
|
2012-10-02 23:07:00 +02:00
|
|
|
|
2015-11-30 20:26:03 +01:00
|
|
|
if (avc_open(NULL, 0) != 0) {
|
2020-03-02 21:12:18 +01:00
|
|
|
int saved_errno = errno;
|
|
|
|
const bool enforce = mac_selinux_enforcing();
|
2012-09-06 22:23:11 +02:00
|
|
|
|
2020-03-02 21:12:18 +01:00
|
|
|
log_full_errno(enforce ? LOG_ERR : LOG_WARNING, saved_errno, "Failed to open the SELinux AVC: %m");
|
2012-09-06 22:23:11 +02:00
|
|
|
|
2015-11-30 20:26:03 +01:00
|
|
|
/* If enforcement isn't on, then let's suppress this
|
|
|
|
* error, and just don't do any AVC checks. The
|
|
|
|
* warning we printed is hence all the admin will
|
|
|
|
* see. */
|
2020-03-02 21:12:18 +01:00
|
|
|
if (!enforce)
|
2015-11-30 20:26:03 +01:00
|
|
|
return 0;
|
2012-09-06 22:23:11 +02:00
|
|
|
|
2015-11-30 20:26:03 +01:00
|
|
|
/* Return an access denied error, if we couldn't load
|
|
|
|
* the AVC but enforcing mode was on, or we couldn't
|
|
|
|
* determine whether it is one. */
|
2019-07-03 16:56:17 +02:00
|
|
|
return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Failed to open the SELinux AVC: %s", strerror_safe(saved_errno));
|
2015-11-30 20:26:03 +01:00
|
|
|
}
|
2013-11-19 21:12:59 +01:00
|
|
|
|
2015-11-30 20:26:03 +01:00
|
|
|
selinux_set_callback(SELINUX_CB_AUDIT, (union selinux_callback) audit_callback);
|
|
|
|
selinux_set_callback(SELINUX_CB_LOG, (union selinux_callback) log_callback);
|
2012-09-06 22:23:11 +02:00
|
|
|
|
2012-10-02 23:07:00 +02:00
|
|
|
initialized = true;
|
2015-11-30 20:26:03 +01:00
|
|
|
return 1;
|
2012-09-06 22:23:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
This function communicates with the kernel to check whether or not it should
|
|
|
|
allow the access.
|
|
|
|
If the machine is in permissive mode it will return ok. Audit messages will
|
|
|
|
still be generated if the access would be denied in enforcing mode.
|
|
|
|
*/
|
2014-10-24 14:15:25 +02:00
|
|
|
int mac_selinux_generic_access_check(
|
2013-11-19 21:12:59 +01:00
|
|
|
sd_bus_message *message,
|
2012-10-02 23:07:00 +02:00
|
|
|
const char *path,
|
|
|
|
const char *permission,
|
2013-11-19 21:12:59 +01:00
|
|
|
sd_bus_error *error) {
|
2012-10-02 23:07:00 +02:00
|
|
|
|
tree-wide: expose "p"-suffix unref calls in public APIs to make gcc cleanup easy
GLIB has recently started to officially support the gcc cleanup
attribute in its public API, hence let's do the same for our APIs.
With this patch we'll define an xyz_unrefp() call for each public
xyz_unref() call, to make it easy to use inside a
__attribute__((cleanup())) expression. Then, all code is ported over to
make use of this.
The new calls are also documented in the man pages, with examples how to
use them (well, I only added docs where the _unref() call itself already
had docs, and the examples, only cover sd_bus_unrefp() and
sd_event_unrefp()).
This also renames sd_lldp_free() to sd_lldp_unref(), since that's how we
tend to call our destructors these days.
Note that this defines no public macro that wraps gcc's attribute and
makes it easier to use. While I think it's our duty in the library to
make our stuff easy to use, I figure it's not our duty to make gcc's own
features easy to use on its own. Most likely, client code which wants to
make use of this should define its own:
#define _cleanup_(function) __attribute__((cleanup(function)))
Or similar, to make the gcc feature easier to use.
Making this logic public has the benefit that we can remove three header
files whose only purpose was to define these functions internally.
See #2008.
2015-11-27 19:13:45 +01:00
|
|
|
_cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL;
|
2020-02-06 21:16:08 +01:00
|
|
|
const char *tclass, *scon;
|
2013-11-28 17:50:02 +01:00
|
|
|
_cleanup_free_ char *cl = NULL;
|
2020-02-06 21:39:40 +01:00
|
|
|
_cleanup_freecon_ char *fcon = NULL;
|
2013-11-28 17:50:02 +01:00
|
|
|
char **cmdline = NULL;
|
2020-03-17 18:11:14 +01:00
|
|
|
bool enforce;
|
2013-11-19 21:12:59 +01:00
|
|
|
int r = 0;
|
2012-09-18 01:55:49 +02:00
|
|
|
|
2012-10-02 23:07:00 +02:00
|
|
|
assert(message);
|
|
|
|
assert(permission);
|
|
|
|
assert(error);
|
|
|
|
|
2015-11-30 20:26:03 +01:00
|
|
|
r = access_init(error);
|
|
|
|
if (r <= 0)
|
2012-10-02 23:56:54 +02:00
|
|
|
return r;
|
|
|
|
|
2020-03-17 18:11:14 +01:00
|
|
|
/* delay call until we checked in `access_init()` if SELinux is actually enabled */
|
|
|
|
enforce = mac_selinux_enforcing();
|
|
|
|
|
2013-11-28 17:50:02 +01:00
|
|
|
r = sd_bus_query_sender_creds(
|
|
|
|
message,
|
bus: use EUID over UID and fix unix-creds
Whenever a process performs an action on an object, the kernel uses the
EUID of the process to do permission checks and to apply on any newly
created objects. The UID of a process is only used if someone *ELSE* acts
on the process. That is, the UID of a process defines who owns the
process, the EUID defines what privileges are used by this process when
performing an action.
Process limits, on the other hand, are always applied to the real UID, not
the effective UID. This is, because a process has a user object linked,
which always corresponds to its UID. A process never has a user object
linked for its EUID. Thus, accounting (and limits) is always done on the
real UID.
This commit fixes all sd-bus users to use the EUID when performing
privilege checks and alike. Furthermore, it fixes unix-creds to be parsed
as EUID, not UID (as the kernel always takes the EUID on UDS). Anyone
using UID (eg., to do user-accounting) has to fall back to the EUID as UDS
does not transmit the UID.
2015-01-18 13:55:55 +01:00
|
|
|
SD_BUS_CREDS_PID|SD_BUS_CREDS_EUID|SD_BUS_CREDS_EGID|
|
2013-11-28 17:50:02 +01:00
|
|
|
SD_BUS_CREDS_CMDLINE|SD_BUS_CREDS_AUDIT_LOGIN_UID|
|
2014-11-26 18:57:37 +01:00
|
|
|
SD_BUS_CREDS_SELINUX_CONTEXT|
|
|
|
|
SD_BUS_CREDS_AUGMENT /* get more bits from /proc */,
|
2013-11-28 17:50:02 +01:00
|
|
|
&creds);
|
|
|
|
if (r < 0)
|
2020-02-06 21:39:40 +01:00
|
|
|
return r;
|
2012-09-06 22:23:11 +02:00
|
|
|
|
2015-04-21 00:58:08 +02:00
|
|
|
/* The SELinux context is something we really should have
|
|
|
|
* gotten directly from the message or sender, and not be an
|
|
|
|
* augmented field. If it was augmented we cannot use it for
|
|
|
|
* authorization, since this is racy and vulnerable. Let's add
|
|
|
|
* an extra check, just in case, even though this really
|
|
|
|
* shouldn't be possible. */
|
|
|
|
assert_return((sd_bus_creds_get_augmented_mask(creds) & SD_BUS_CREDS_SELINUX_CONTEXT) == 0, -EPERM);
|
|
|
|
|
2013-11-28 17:50:02 +01:00
|
|
|
r = sd_bus_creds_get_selinux_context(creds, &scon);
|
2013-11-19 21:12:59 +01:00
|
|
|
if (r < 0)
|
2020-02-06 21:39:40 +01:00
|
|
|
return r;
|
2012-10-02 23:07:00 +02:00
|
|
|
|
2012-09-06 22:23:11 +02:00
|
|
|
if (path) {
|
2013-11-19 21:12:59 +01:00
|
|
|
/* Get the file context of the unit file */
|
|
|
|
|
2020-02-06 21:39:40 +01:00
|
|
|
if (getfilecon_raw(path, &fcon) < 0) {
|
|
|
|
r = -errno;
|
|
|
|
|
2020-05-13 21:28:04 +02:00
|
|
|
log_warning_errno(r, "SELinux getfilecon_raw() on '%s' failed%s (perm=%s): %m",
|
2020-02-06 21:39:40 +01:00
|
|
|
path,
|
|
|
|
enforce ? "" : ", ignoring",
|
|
|
|
permission);
|
|
|
|
if (!enforce)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Failed to get file context on %s.", path);
|
2012-09-06 22:23:11 +02:00
|
|
|
}
|
|
|
|
|
2013-11-19 21:12:59 +01:00
|
|
|
tclass = "service";
|
2020-02-06 21:39:40 +01:00
|
|
|
|
2012-09-06 22:23:11 +02:00
|
|
|
} else {
|
2020-02-06 21:39:40 +01:00
|
|
|
if (getcon_raw(&fcon) < 0) {
|
|
|
|
r = -errno;
|
|
|
|
|
2020-05-13 21:28:04 +02:00
|
|
|
log_warning_errno(r, "SELinux getcon_raw() failed%s (perm=%s): %m",
|
2020-02-06 21:39:40 +01:00
|
|
|
enforce ? "" : ", ignoring",
|
|
|
|
permission);
|
|
|
|
if (!enforce)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Failed to get current context.");
|
2012-09-06 22:23:11 +02:00
|
|
|
}
|
2013-11-19 21:12:59 +01:00
|
|
|
|
|
|
|
tclass = "system";
|
2012-09-06 22:23:11 +02:00
|
|
|
}
|
|
|
|
|
2013-11-28 17:50:02 +01:00
|
|
|
sd_bus_creds_get_cmdline(creds, &cmdline);
|
|
|
|
cl = strv_join(cmdline, " ");
|
|
|
|
|
2020-02-06 21:39:40 +01:00
|
|
|
struct audit_info audit_info = {
|
|
|
|
.creds = creds,
|
|
|
|
.path = path,
|
|
|
|
.cmdline = cl,
|
|
|
|
};
|
2012-09-06 22:23:11 +02:00
|
|
|
|
2015-05-25 02:20:06 +02:00
|
|
|
r = selinux_check_access(scon, fcon, tclass, permission, &audit_info);
|
2020-02-06 21:39:40 +01:00
|
|
|
if (r < 0) {
|
|
|
|
r = errno_or_else(EPERM);
|
2012-09-06 22:23:11 +02:00
|
|
|
|
2020-02-06 21:39:40 +01:00
|
|
|
if (enforce)
|
|
|
|
sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "SELinux policy denies access.");
|
2012-09-06 22:23:11 +02:00
|
|
|
}
|
|
|
|
|
2020-03-26 20:15:17 +01:00
|
|
|
log_debug_errno(r, "SELinux access check scon=%s tcon=%s tclass=%s perm=%s state=%s path=%s cmdline=%s: %m",
|
|
|
|
scon, fcon, tclass, permission, enforce ? "enforcing" : "permissive", path, cl);
|
2020-02-06 21:39:40 +01:00
|
|
|
return enforce ? r : 0;
|
2015-11-30 20:26:03 +01:00
|
|
|
}
|
|
|
|
|
2014-10-24 14:15:25 +02:00
|
|
|
#else
|
2015-11-30 20:26:03 +01:00
|
|
|
|
|
|
|
int mac_selinux_generic_access_check(
|
|
|
|
sd_bus_message *message,
|
|
|
|
const char *path,
|
|
|
|
const char *permission,
|
|
|
|
sd_bus_error *error) {
|
|
|
|
|
2014-10-24 14:15:25 +02:00
|
|
|
return 0;
|
2012-09-06 22:23:11 +02:00
|
|
|
}
|
2015-11-30 20:26:03 +01:00
|
|
|
|
|
|
|
#endif
|