audit.c revision 794f0adb050e571bbfde4d2a19b9f88b852079dd
/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
*/
/*
* This file contains the audit hook support code for auditing.
*/
#include <sys/pathname.h>
#include <sys/ipc_impl.h>
#include <sys/msg_impl.h>
#include <sys/sem_impl.h>
#include <sys/shm_impl.h>
#include <c2/audit_kevents.h>
#include <c2/audit_record.h>
#include <sys/devpolicy.h>
#include <sys/cred_impl.h>
#include <net/pfpolicy.h>
/*
* ROUTINE: AUDIT_SAVEPATH
* PURPOSE:
* CALLBY: LOOKUPPN
*
* We get two pieces of information here:
* the vnode of the last component (vp) and
* the status of the last access (flag).
* TODO:
* QUESTION:
*/
/*ARGSUSED*/
int
int flag, /* status of the last access */
{
/*
* Noise elimination in audit trails - this event will be discarded if:
* - the public policy is not active AND
* - the system call is a public operation AND
* - the file was not found: VFS lookup failed with ENOENT error AND
* - the missing file would have been located in the public directory
* owned by root if it had existed
*/
if (object_is_public(&attr)) {
}
}
}
/*
* this event being audited or do we need path information
* to file pointer. If the path has already been found for an
*
* S2E_SP (TAD_SAVPATH) flag comes from audit_s2e[].au_ctrl. Used with
* chroot, chdir, open, creat system call processing. It determines
* if audit_savepath() will discard the path or we need it later.
* TAD_PATHFND means path already included in this audit record. It
* is used in cases where multiple path lookups are done per
* system call. The policy flag, AUDIT_PATH, controls if multiple
* paths are allowed.
* S2E_NPT (TAD_NOPATH) flag comes from audit_s2e[].au_ctrl. Used with
* exit processing to inhibit any paths that may be added due to
* closes.
*/
return (0);
}
/*
* are we auditing only if error, or if it is not open or create
* otherwise audit_setf will do it
*/
if (flag &&
}
/* add token to audit record for this name */
/* add the attributes of the object */
if (vp) {
/*
* only capture attributes when there is no error
* lookup will not return the vnode of the failing
* component.
*
* if there was a lookup error, then don't add
* attribute. if lookup in vn_create(),
* then don't add attribute,
* it will be added at end of vn_create().
*/
}
}
/* free up space if we're not going to save path (open, creat) */
}
}
return (0);
}
static void
{
char *pp; /* pointer to path */
int len; /* length of incoming segment */
int newsect; /* path requires a new section */
/* adjust for path prefix: tad_aupath, ATPATH, CRD, or CWD */
} else {
}
/* get an expanded buffer to hold the anchored path */
if (!newsect) {
/* overlay previous NUL terminator */
}
/* now add string of processed path */
/* perform path simplification as necessary */
if (tad->tad_aupath)
/* for case where multiple lookups in one syscall (rename) */
}
/*
* ROUTINE: AUDIT_ANCHORPATH
* PURPOSE:
* CALLBY: LOOKUPPN
* NOTE:
* anchor path at "/". We have seen a symbolic link or entering for the
* first time we will throw away any saved path if path is anchored.
*
* flag = 0, path is relative.
* flag = 1, path is absolute. Free any saved path and set flag to TAD_ABSPATH.
*
* If the (new) path is absolute, then we have to throw away whatever we have
* already accumulated since it is being superseded by new path which is
* anchored at the root.
* Note that if the path is relative, this function does nothing
* TODO:
* QUESTION:
*/
/*ARGSUSED*/
void
{
/*
* this event being audited or do we need path information
* to file pointer. If the path has already been found for an
*
* S2E_SP (TAD_SAVPATH) flag comes from audit_s2e[].au_ctrl. Used with
* chroot, chdir, open, creat system call processing. It determines
* if audit_savepath() will discard the path or we need it later.
* TAD_PATHFND means path already included in this audit record. It
* is used in cases where multiple path lookups are done per
* system call. The policy flag, AUDIT_PATH, controls if multiple
* paths are allowed.
* S2E_NPT (TAD_NOPATH) flag comes from audit_s2e[].au_ctrl. Used with
* exit processing to inhibit any paths that may be added due to
* closes.
*/
return;
}
if (flag) {
}
}
}
/*
* symbolic link. Save previous components.
*
* the path seen so far looks like this
*
* +-----------------------+----------------+
* | path processed so far | remaining path |
* +-----------------------+----------------+
* \-----------------------/
* save this string if
* symbolic link relative
* (but don't include symlink component)
*/
/*ARGSUSED*/
/*
* ROUTINE: AUDIT_SYMLINK
* PURPOSE:
* CALLBY: LOOKUPPN
* NOTE:
* TODO:
* QUESTION:
*/
void
{
char *sp; /* saved initial pp */
char *cp; /* start of symlink path */
/*
* this event being audited or do we need path information
* to file pointer. If the path has already been found for an
*
* S2E_SP (TAD_SAVPATH) flag comes from audit_s2e[].au_ctrl. Used with
* chroot, chdir, open, creat system call processing. It determines
* if audit_savepath() will discard the path or we need it later.
* TAD_PATHFND means path already included in this audit record. It
* is used in cases where multiple path lookups are done per
* system call. The policy flag, AUDIT_PATH, controls if multiple
* paths are allowed.
* S2E_NPT (TAD_NOPATH) flag comes from audit_s2e[].au_ctrl. Used with
* exit processing to inhibit any paths that may be added due to
* closes.
*/
return;
}
/*
* if symbolic link is anchored at / then do nothing.
* When we cycle back to begin: in lookuppn() we will
* call audit_anchorpath() with a flag indicating if the
* path is anchored at / or is relative. We will release
* any saved path at that point.
*
* Note In the event that an error occurs in pn_combine then
* we want to remain pointing at the component that caused the
* path to overflow the pnp structure.
*/
return;
/* backup over last component */
;
/* is there anything to save? */
if (len_path) {
}
}
/*
* object_is_public : determine whether events for the object (corresponding to
* ignored.
*
* returns: 1 - if audit policy and object attributes indicate that
* the file should not be audited.
* 0 - otherwise
*
* The required attributes to be considered a public object are:
* - owned by root, AND
* - world-readable (permissions for other include read), AND
* - NOT world-writeable (permissions for other don't
* include write)
* (mode doesn't need to be checked for symlinks)
*/
int
{
return (1);
}
return (0);
}
/*
* ROUTINE: AUDIT_ATTRIBUTES
* PURPOSE: Audit the attributes so we can tell why the error occurred
* CALLBY: AUDIT_SAVEPATH
* AUDIT_VNCREATE_FINISH
* NOTE:
* TODO:
* QUESTION:
*/
void
{
struct t_audit_data *tad;
if (vp) {
return;
if (object_is_public(&attr) &&
/*
* This is a public object and a "public" event
* (i.e., read only) -- either by definition
* (e.g., stat, access...) or by virtue of write access
* not being requested (e.g. mmap).
* Flag it in the tad to prevent this audit at the end.
*/
} else {
}
}
}
/*
* ROUTINE: AUDIT_EXIT
* PURPOSE:
* CALLBY: EXIT
* NOTE:
* TODO:
* QUESTION: why cmw code as offset by 2 but not here
*/
/* ARGSUSED */
void
{
struct t_audit_data *tad;
/*
* tad_scid will be set by audit_start even if we are not auditing
* the event.
*/
/*
* if we are auditing the exit system call, then complete
* audit record generation (no return from system call).
*/
audit_finish(0, SYS_exit, 0, 0);
return;
}
/*
* Anyone auditing the system call that was aborted?
*/
}
/*
* Generate an audit record for process exit if preselected.
*/
audit_finish(0, SYS_exit, 0, 0);
}
/*
* ROUTINE: AUDIT_CORE_START
* PURPOSE:
* CALLBY: PSIG
* NOTE:
* TODO:
*/
void
audit_core_start(int sig)
{
kctx = GET_KCTX_PZ;
/* get basic event for system call */
return;
/* reset the flags for non-user attributable events */
/* if auditing not enabled, then don't generate an audit record */
return;
}
}
/*
* ROUTINE: AUDIT_CORE_FINISH
* PURPOSE:
* CALLBY: PSIG
* NOTE:
* TODO:
* QUESTION:
*/
/*ARGSUSED*/
void
audit_core_finish(int code)
{
int flag;
return;
}
kctx = GET_KCTX_PZ;
/* kludge for error 0, should use `code==CLD_DUMPED' instead */
/*
* Add subject information (no locks since our private copy of
* credential
*/
/* Add a return token (should use f argument) */
}
/* Close up everything */
/* free up any space remaining with the path's */
}
}
/*ARGSUSED*/
void
{
/* lock stdata from audit_sock */
/* proceed ONLY if user is being audited */
/*
* this is so we will not add audit data onto
* a thread that is not being audited.
*/
return;
}
}
/*ARGSUSED*/
void
{
/* lock stdata from audit_sock */
/* proceed ONLY if user is being audited */
/*
* this is so we will not add audit data onto
* a thread that is not being audited.
*/
return;
}
}
/*
* ROUTINE: AUDIT_CLOSEF
* PURPOSE:
* CALLBY: CLOSEF
* NOTE:
* release per file audit resources when file structure is being released.
*
* IMPORTANT NOTE: Since we generate an audit record here, we may sleep
* on the audit queue if it becomes full. This means
* audit_closef can not be called when f_count == 0. Since
* f_count == 0 indicates the file structure is free, another
* process could attempt to use the file while we were still
* asleep waiting on the audit queue. This would cause the
* per file audit data to be corrupted when we finally do
* wakeup.
* TODO:
* QUESTION:
*/
void
{ /* AUDIT_CLOSEF */
int success;
const auditinfo_addr_t *ainfo;
int getattr_ret;
/* audit record already generated by system call envelope */
/* so close audit event will have bits set */
return;
}
/* if auditing not enabled, then don't generate an audit record */
return;
return;
/* not selected for this event */
if (success == 0)
return;
/*
* can't use audit_attributes here since we use a private audit area
* to build the audit record instead of the one off the thread.
*/
}
/*
* When write was not used and the file can be considered public,
* then skip the audit.
*/
if (object_is_public(&attr)) {
return;
}
}
} else {
#ifdef _LP64
#else
#endif
}
if (getattr_ret == 0) {
}
/* Add subject information */
/* add a return token */
/*
* Close up everything
* Note: path space recovery handled by normal system
* call envelope if not at last close.
* Note there is no failure at this point since
* this represents closes due to exit of process,
* thus we always indicate successful closes.
*/
}
/*
* ROUTINE: AUDIT_SET
* PURPOSE: Audit the file path and file attributes.
* CALLBY: SETF
* NOTE: SETF associate a file pointer with user area's open files.
* TODO:
* call audit_finish directly ???
* QUESTION:
*/
/*ARGSUSED*/
void
{
return;
return;
/* no path */
if (tad->tad_aupath == 0)
return;
/*
* assign path information associated with file audit data
* use tad hold
*/
/* adjust event type by dropping the 'creat' part */
case AUE_OPEN_RC:
break;
case AUE_OPEN_RTC:
break;
case AUE_OPEN_WC:
break;
case AUE_OPEN_WTC:
break;
case AUE_OPEN_RWC:
break;
case AUE_OPEN_RWTC:
break;
default:
break;
}
}
}
void
{
/* if not auditing this event, then do nothing */
if (ad_flag == 0)
return;
switch (type) {
case AT_IPC_MSG:
break;
case AT_IPC_SEM:
break;
case AT_IPC_SHM:
break;
}
}
void
{
/* if not auditing this event, then do nothing */
if (ad_flag == 0)
return;
switch (type) {
case NULL:
break;
case AT_IPC_MSG:
break;
case AT_IPC_SEM:
break;
case AT_IPC_SHM:
break;
}
}
/*
* ROUTINE: AUDIT_REBOOT
* PURPOSE:
* CALLBY:
* NOTE:
* At this point we know that the system call reboot will not return. We thus
* have to complete the audit record generation and put it onto the queue.
* This might be fairly useless if the auditing daemon is already dead....
* TODO:
* QUESTION: who calls audit_reboot
*/
void
audit_reboot(void)
{
int flag;
/* if not auditing this event, then do nothing */
return;
/* add a process token */
return;
/* Add subject information */
/* add a return token */
}
/*
* Flow control useless here since we're going
* to drop everything in the queue anyway. Why
* block and wait. There aint anyone left alive to
* read the records remaining anyway.
*/
/* Close up everything */
}
void
audit_setfsat_path(int argnum)
{
struct f_audit_data *fad;
struct a {
long arg1;
long arg2;
long arg3;
long arg4;
long arg5;
} *uap;
return;
case SYS_faccessat:
case SYS_fchmodat:
case SYS_fchownat:
case SYS_fstatat:
case SYS_fstatat64:
case SYS_mkdirat:
case SYS_mknodat:
case SYS_openat:
case SYS_openat64:
case SYS_readlinkat:
case SYS_unlinkat:
break;
case SYS_linkat:
case SYS_renameat:
if (argnum == 3)
else
break;
case SYS_symlinkat:
case SYS_utimesys:
break;
case SYS_open:
case SYS_open64:
break;
default:
return;
}
}
}
}
return;
}
return;
}
} else {
}
return;
}
}
}
void
{
/* if not auditing this event, then do nothing */
return;
if (error)
return;
if (error == 0) {
}
}
/*
* ROUTINE: AUDIT_VNCREATE_START
* PURPOSE: set flag so path name lookup in create will not add attribute
* CALLBY: VN_CREATE
* NOTE:
* TODO:
* QUESTION:
*/
void
{
}
/*
* ROUTINE: AUDIT_VNCREATE_FINISH
* PURPOSE:
* CALLBY: VN_CREATE
* NOTE:
* TODO:
* QUESTION:
*/
void
{
if (error)
return;
/* if not auditing this event, then do nothing */
return;
}
}
}
/* for case where multiple lookups in one syscall (rename) */
}
/*
* ROUTINE: AUDIT_EXEC
* PURPOSE: Records the function arguments and environment variables
* CALLBY: EXEC_ARGS
* NOTE:
* TODO:
* QUESTION:
*/
void
const char *argstr, /* argument strings */
const char *envstr, /* environment strings */
{
/* if not auditing this event, then do nothing */
return;
/* It's a different event. */
/* Add the current working directory to the audit trail. */
/*
* The new credential is not yet in place when audit_exec
* is called.
* Compute the additional bits available in the new credential
* and the limit set.
*/
priv_inverse(&pset);
if (!priv_isemptyset(&pset) ||
0));
}
/*
* Compare the uids & gids: create a process token if changed.
*/
}
}
}
/*
* ROUTINE: AUDIT_ENTERPROM
* PURPOSE:
* CALLBY: KBDINPUT
* ZSA_XSINT
* NOTE:
* TODO:
* QUESTION:
*/
void
audit_enterprom(int flg)
{
int sorf;
if (flg)
else
if (flg)
else
}
/*
* ROUTINE: AUDIT_EXITPROM
* PURPOSE:
* CALLBY: KBDINPUT
* ZSA_XSINT
* NOTE:
* TODO:
* QUESTION:
*/
void
audit_exitprom(int flg)
{
int sorf;
if (flg)
else
if (flg)
else
}
struct fcntla {
int fdes;
int cmd;
};
/*
* ROUTINE: AUDIT_CHDIREC
* PURPOSE:
* CALLBY: CHDIREC
* NOTE: The main function of CHDIREC
* TODO: Move the audit_chdirec hook above the VN_RELE in vncalls.c
* QUESTION:
*/
/*ARGSUSED*/
void
{
int chdir;
int fchdir;
struct audit_path **appp;
struct a {
long fd;
if (tad->tad_aupath) {
if (chdir)
else
au_pathrele(*appp);
/* use tad hold */
}
return;
if (fad->fad_aupath) {
if (fchdir)
else
au_pathrele(*appp);
}
}
}
}
/*
* Audit hook for stream based socket and tli request.
* Note that we do not have user context while executing
* this code so we had to record them earlier during the
*/
/*ARGSUSED*/
void
queue_t *q, /* contains the process and thread audit data */
int from) /* timod or sockmod request */
{
struct sockaddr_in *sock_data;
struct T_conn_req *conn_req;
struct T_conn_ind *conn_ind;
struct T_unitdata_req *unitdata_req;
struct T_unitdata_ind *unitdata_ind;
const auditinfo_addr_t *ainfo;
return;
/* are we being audited */
/* no pointer to thread, nothing to do */
if (saved_thread_ptr == NULL) {
return;
}
/* only allow one addition of a record token */
/*
* thread is not the one being audited, then nothing to do
* This could be the stream thread handling the module
* service routine. In this case, the context for the audit
* record can no longer be assumed. Simplest to just drop
* the operation.
*/
return;
}
return;
}
/*
* one running. Now we can get the TAD and see if we should
* add an audit token.
*/
kctx = GET_KCTX_PZ;
/* proceed ONLY if user is being audited */
return;
return;
/*
* Figure out the type of stream networking request here.
* Note that getmsg and putmsg are always preselected
* because during the beginning of the system call we have
* not yet figure out which of the socket or tli request
* we are looking at until we are here. So we need to check
* against that specific request and reset the type of event.
*/
switch (type) {
case T_CONN_REQ: /* connection request */
return;
break;
} else {
return;
}
case T_CONN_IND: /* connectionless receive request */
return;
break;
} else {
return;
}
case T_UNITDATA_REQ: /* connectionless send request */
return;
break;
} else {
return;
}
case T_UNITDATA_IND: /* connectionless receive request */
return;
break;
} else {
return;
}
default:
return;
}
/*
* we are only interested in tcp stream connections,
* not unix domain stuff
*/
return;
}
/* skip over TPI header and point to the ip address */
switch (sock_data->sin_family) {
case AF_INET:
break;
default: /* reset to AUE_PUTMSG if not a inet request */
break;
}
}
static void
{
unsigned int sy_flags;
#ifdef _SYSCALL32_IMPL
/*
* Guard against t_lwp being NULL when this function is called
* from a kernel queue instead of from a direct system call.
* In that case, assume the running kernel data model.
*/
else
#else
#endif
else
}
/*ARGSUSED*/
void
int fd;
int error; /* ignore for now */
{
/* is this system call being audited */
return;
/* add path and file attributes */
} else {
#ifdef _LP64
#else
#endif
}
}
/*
* Record privileges successfully used and we attempted to use but
* didn't have.
*/
void
{
int sbit;
/* Make sure this isn't being called in an interrupt context */
ASSERT(servicing_interrupt() == 0);
return;
/* Tell audit_success() and audit_finish() that we saw this case */
/* Clear set first time around */
}
/* Save the privileges in the tad */
} else {
}
}
/*
* Audit the setpriv() system call; the operation, the set name and
* the current value as well as the set argument are put in the
* audit trail.
*/
void
{
const priv_set_t *oldpriv;
const char *setname;
return;
/* Generate the actual record, include the before and after */
switch (op) {
case PRIV_OFF:
/* Report privileges actually switched off */
break;
case PRIV_ON:
/* Report privileges actually switched on */
break;
case PRIV_SET:
/* Report before and after */
break;
}
}
/*
* Dump the full device policy setting in the audit trail.
*/
void
{
int i;
return;
for (i = 0; i < nitems; i++) {
} else
AUT_PRIV, 0));
AUT_PRIV, 0));
}
}
/*ARGSUSED*/
void
int fd;
{
/* is this system call being audited */
return;
/* add path and file attributes */
} else {
#ifdef _LP64
#else
#endif
}
}
/*
* ROUTINE: AUDIT_CRYPTOADM
* CALLBY: CRYPTO_LOAD_DEV_DISABLED, CRYPTO_LOAD_SOFT_DISABLED,
* CRYPTO_UNLOAD_SOFT_MODULE, CRYPTO_LOAD_SOFT_CONFIG,
* CRYPTO_POOL_CREATE, CRYPTO_POOL_WAIT, CRYPTO_POOL_RUN,
* CRYPTO_LOAD_DOOR
* NOTE:
* TODO:
* QUESTION:
*/
void
{
return;
return;
return;
/* Add subject information */
switch (cmd) {
case CRYPTO_LOAD_DEV_DISABLED:
"op=CRYPTO_LOAD_DEV_DISABLED, module=%s,"
" dev_instance=%d",
} else {
"op=CRYPTO_LOAD_DEV_DISABLED, return_val=%d", rv);
}
break;
"op=CRYPTO_LOAD_SOFT_DISABLED, module=%s",
} else {
"op=CRYPTO_LOAD_SOFT_DISABLED, return_val=%d", rv);
}
break;
"op=CRYPTO_UNLOAD_SOFT_MODULE, module=%s",
} else {
"op=CRYPTO_UNLOAD_SOFT_MODULE, return_val=%d", rv);
}
break;
case CRYPTO_LOAD_SOFT_CONFIG:
"op=CRYPTO_LOAD_SOFT_CONFIG, module=%s",
} else {
"op=CRYPTO_LOAD_SOFT_CONFIG, return_val=%d", rv);
}
break;
case CRYPTO_POOL_CREATE:
"op=CRYPTO_POOL_CREATE");
break;
case CRYPTO_POOL_WAIT:
break;
case CRYPTO_POOL_RUN:
break;
case CRYPTO_LOAD_DOOR:
"op=CRYPTO_LOAD_DOOR");
else
"op=CRYPTO_LOAD_DOOR, return_val=%d", rv);
break;
case CRYPTO_FIPS140_SET:
"op=CRYPTO_FIPS140_SET, fips_state=%d", rv);
break;
default:
return;
}
if (mech_list_required) {
int i;
if (mech_count == 0) {
} else {
size_t n;
for (i = 0; i < mech_count; i++) {
pb += n;
l -= n;
if (l < 0)
l = 0;
if (i == mech_count - 1)
space);
}
}
}
/* add a return token */
else
NULL);
}
/*
* Audit the kernel SSL administration command. The address and the
* port number for the SSL instance, and the proxy port are put in the
* audit trail.
*/
void
{
return;
return;
/* Add subject information */
switch (cmd) {
case KSSL_ADD_ENTRY: {
char buf[32];
break;
}
case KSSL_DELETE_ENTRY: {
char buf[32];
break;
}
default:
return;
}
/* add a return token */
NULL);
}
/*
* Audit the kernel PF_POLICY administration commands. Record command,
* zone, policy type (global or tunnel, active or inactive)
*/
/*
* ROUTINE: AUDIT_PF_POLICY
* PURPOSE: Records arguments to administrative ioctls on PF_POLICY socket
* CALLBY: SPD_ADDRULE, SPD_DELETERULE, SPD_FLUSH, SPD_UPDATEALGS,
* SPD_CLONE, SPD_FLIP
* NOTE:
* TODO:
* QUESTION:
*/
void
{
const auditinfo_addr_t *ainfo;
char buf[80];
int flag;
return;
return;
/*
* Initialize some variables since these are only set
* with system calls.
*/
switch (cmd) {
case SPD_ADDRULE: {
break;
}
case SPD_DELETERULE: {
break;
}
case SPD_FLUSH: {
break;
}
case SPD_UPDATEALGS: {
break;
}
case SPD_CLONE: {
break;
}
case SPD_FLIP: {
break;
}
default:
}
/*
* For now, just audit that an event happened,
* along with the error code.
*/
/* Supplemental data */
/*
* Generate this zone token if the target zone differs
* from the administrative zone. If netstacks are expanded
* to something other than a 1-1 relationship with zones,
* the auditing framework should create a new token type
* and audit it as a netstack instead.
* Turn on general zone auditing to get the administrative zone.
*/
ns->netstack_stackid));
}
}
/* write tunnel name - tun is bounded */
tun);
}
/* Add subject information */
/* add a return token */
}
NULL);
/*
* clear the ctrl flag so that we don't have spurious collection of
* audit information.
*/
}
/*
* ROUTINE: AUDIT_SEC_ATTRIBUTES
* PURPOSE: Add security attributes
* CALLBY: AUDIT_ATTRIBUTES
* AUDIT_CLOSEF
* AUS_CLOSE
* NOTE:
* TODO:
* QUESTION:
*/
void
{
/* Dump the SL */
if (is_system_labeled()) {
return; /* nothing else to do */
return; /* nothing else to do */
}
} /* AUDIT_SEC_ATTRIBUTES */