MULTICS TECHNICAL BULLETIN 681-01 page 1
To: Distribution
From: Keith Loepere
Date: November 21, 1984
Subject: Restructuring Directory Control
As a part of the B2 effort, directory control is being
restructured. The major justification for this restructuring is
to centralize the security related software within ring zero. In
particular, access checking and security auditing are to be
centralized. This revision of MTB 681 describes the efforts |
taken in this direction for MR11. |
This MTB describes the MR10.2 structuring of directory |
control along with problems that occur largely as a result of |
this structuring. The MR11 structure of directory control is |
then described, along with new error code censoring policies
introduced and other incompatible features of this design. The
finale concerns itself with unsolved problems within directory
control.
This MTB assumes that the reader understands many of the
concepts found within directory control.
Comments on this MTB should be sent to the author:
Keith Loepere (Loepere.Multics)
or via the B2 forum.
_________________________________________________________________
Multics Project internal working documentation. Not to be
reproduced or distributed outside the Multics Project.
PREFACE
As a result of the B2 evaluation effort, a variety of
problems with the file system (mostly directory control) have
been discovered. These problems consist of a collection of bugs,
plus a lack of consistency in our enforcement of our access
model, as well as a lack of consistency within our model of
access control. These problems need to be addressed to various
degrees to meet the B2 requirements. Our answer to these
problems within the file system are being met in a series of
phases.
The first phase consisted of an analysis of the current
behavior of the primitives within the file system. This analysis
was done to determine the set of access checks performed by the
various file system primitives and the manner by which they are
performed. The current structuring of the file system was
discovered. The current access model is now known. The results
of the first phase (as well as the changes resulting from the
work described by this MTB) will appear in an upcoming File
System SDN.
| The second phase was described in the first revision of this
| MTB (and is also included in this revision). It involves
centralizing the access checking software within directory con-
trol. This is being done to ensure that access checks are being
done correctly and with proper auditing of attempted access
violations. The access checks currently performed by the file
system are not being changed, except in those cases where a
primitive failed to perform its access check. Within this
process, various bugs, as well as a design error in our policy
toward error code censoring, will be fixed.
| The third phase involves adding support to directory control
| to not only audit attempted access violations but also to audit
| successful accesses. Also, all access computations within ring
| zero were checked for validity. The result of this third phase
| is described in this MTB. (In particular, it is described by the
| material within change bars.)
The last phase of directory control restructuring involves
creating a consistent access model (or, at least, a more consis-
tent access model). The system would then be converted to work
within this model. This pass through the system would also
| include fixing more bugs within directory control and its
| interfaces. It is unknown when, if ever, this will be done.
| The goal the directory control restructuring effort is to
| provide a platform that will enable us to more easily express our
current access policy and more easily demonstrate that it is
enforced. This platform will also make it easier (from the file
system side) to change to a more consistent access model in the
future.
INTRODUCTION
Directory control is that portion of the system that con-
cerns itself with the structuring of the storage system into
directories and segments and controlling access to those objects.
It lies logically above segment and page control, using their
facilities to access the contents of directories as if they were
normal segments. (Directory control is not strictly above seg-
ment control, of course, since segment control does thread aste's
together relative to the hierarchy structure. (That is to say,
segment control knows the difference between directories and
segments.) Segment control also has a path into directory |
control to determine the SDW access control fields of the |
segments segment control controls. This path includes auditing |
of successful accesses to segments.) Directory control contains |
the security mechanism for the file system. Directory control |
lies under address and name space management, in that this latter
subsystem uses directory control to find and determine access to
objects. (Again, directory control is not strictly under address
and name space management in that directory control must bring
various directories into the user's address space to use them for
its own purposes (such as when walking down the hierarchy or when
chasing links). These extra directories enter the user's address
space but not strictly the user's name space. A description of
this area appears later in this MTB.)
For the sake of convenience, if not perfect accuracy, in
this MTB I will refer to the combination of directory control and
address and name space management as the file system. (The file
system will be considered to sit on top of the storage system,
which consists of page control, segment control and volume
management.) As such, the program that is called to perform a
function within the file system, such as creating a branch,
changing an ACL, etc., will be referred to as a file system
primitive. The function it performs is called a file system
function.
A program internal to directory control that performs a
support function for the file system primitives will be referred
to as a directory control primitive. Thus, the program that
finds a directory or the program that audits attempted access
violations will be called a directory control primitive.
A file system primitive, then, does its job by calling a
series of directory control primitives, as well as performing
some operations internal to itself.
The directory control functions that will be described
within this MTB follow. It is a directory control function to
locate directories and specific entries within those directories.
Another function is to determine the user's access (normally the
effective access) to the directory or entry at hand. The
auditing of successful accesses to file system objects, as well |
| as attempted access violations, if necessary, is a directory
| control function. (In this MTB, the phrase "audit an event"
| means that a determination is made as to whether an audit message
| is to be generated to describe the event. Only if this determi-
| nation is positive will an actual audit message be added to the
| security audit log.) After these directory control functions are
performed, the various file system primitives can perform their
function on the directory or the directory entry. These three
directory control functions:
1) directory (or directory entry) finding
2) access checking
3) auditing
are common to all file system functions. I will refer to these
directory control functions as the three basic directory control
functions. They are the main subject of this MTB.
CURRENT STRUCTURING OF DIRECTORY CONTROL
| The file system currently (MR10.2) consists of a set of file
| system primitives that implement the various functions that the
| file system performs (ACL listing, "attribute" setting, and the
| like). The three basic directory control functions are performed
by a group of programs (the directory control primitives) that
are external to, and called by, the file system primitives. A
simplified description of the current operation of the modules
that perform the three basic directory control functions follows.
Directory and Entry Finding
The function of finding a directory (and having address and
name space management bring it into the address space) is
performed by the directory control primitive find_dirsegno. It
takes a pathname and returns the segment number of the directory
as the directory's identity in the address space. In the process
of finding the directory, find_dirsegno brings the directories
superior to the target directory into the user's address space (a
requirement for proper process operation). find_dirsegno also
handles the case where the desired pathname contains a link;
encountering a link forces find_dirsegno to find the target of
the link and then search for the remaining entry names in the
pathname. Note that find_dirsegno is responsible for detecting
attempts to reference an AIM protected directory (one to which
the user fails a read_allowed_ test).
Searching for entries within a directory is performed by the
directory control primitive find_entry. find_entry makes certain
validity checks on a directory entry, including checking for the
object being "security out of service".
Both find_entry and find_dirsegno are not called in normal
cases by the file system primitives. Most file system primitives
call the directory control primitive find_ to locate directories
and directory entries. find_$dir basically consists of a call to
find_dirsegno. find_$entry is basically a call to find_dirsegno
followed by a call to find_entry. find_$branch is the usual
directory control primitive called by most user visible file
system primitives. This directory control primitive is the one
that chases links. It also consists of calls to find_dirsegno
and find_entry. However, if the directory entry located by
find_entry describes a link, find_$branch will proceed to find
the target of the link, and continue to chase such links until a
target is found. (Links in the directory portion of a pathname
are chased by find_dirsegno.)
Another way to find a directory is provided by the directory
control primitives uid_path_util$find and uid_path_util$find_dir.
These entries are used mostly by master directory control, to
find a directory or entry given a uid path.
Access Checking
A normal file system primitive starts by calling
find_$branch or find_$entry to locate the directory entry in
question, depending on whether link chasing was specified or not.
(Programs that manipulate an entire directory, star_,
list_inacl_all, etc., call find_$dir.) The file system primitive
then makes a series of access checks, using the directory control
primitives fs_get or access_mode to compute the access. Each
file system primitive performs its own access checks, checking
for access of whatever kind it wants on the object in question,
its parent, its parent's parent, etc.
Auditing
If an access violation was attempted, an audit message must
be generated. Also, the file system primitive must determine
what error code to return to the user, so as to not return too
much information to the user via this error code. (This error
code is normally error_table_$incorrect_access or
error_table_$no_info.) This process is referred to as error code
censoring. A discussion of the implications of error code
censoring appears later in this MTB. Auditing and error code
censoring are integral functions both performed by the directory
control primitive dir_control_error. Successful accesses to file |
system objects are not currently audited. |
CURRENT ACCESS MODEL (SIMPLIFIED)
In the way of background, the properties of an object within
the file system are considered to come in three categories.
These are the "status", "attributes" and "contents" of an object.
The "contents" of a segment is the set of machine words that
make up the segment. We also sometimes consider the bit count of
a segment to be a "contents" property of the segment. The
"contents" of a directory are the list of names within it and the
initial ACLs of the directory (this data being physically
| contained within the directory). Certain vtoc resident pieces of
| information about a directory are considered as contents
| properties: the quota and time-record product. The bit count
(msf component indicator) of a directory is also sometimes
considered a "contents" property of the directory. A user must
possess specific access on the object itself to attempt to affect
a "contents" property of the object. The access that is required
on the object is a function of the type of file system function
being performed.
The "status" properties of an object are those properties
that are considered as "belonging" to the parent directory of the
object (as opposed to belonging to, or requiring particular
access on, the object). These properties are the names and the
ACL of the object. A user must possess specific access on the
parent directory of an object to attempt to affect a "status"
property of the object. The access that is required on the
parent is a function of the type of file system function being
performed.
The "attributes" properties of an object are basically
everything else; that is, the ring brackets, safety switch,
maximum length, etc. A user can either possess specific access
to the parent directory of an object or can possess specific
access to that object (one or the other or both must be true) to
attempt to affect an "attributes" property of the object. The
access that is required on the object or on the parent is a
function of the type of file system function being performed.
The complete description of our access model (the entire set
of access checks within ring zero) will appear in the upcoming
File System SDN.
PROBLEMS WITH THE CURRENT STRUCTURING
The biggest problem with the current structuring of the
basic directory control primitives is that their execution is
dependent on their being called by the file system primitive in
question. The finding of directories and directory entries is
fine (except for the implications of directories appearing in the
address space, discussed later). It is the other two functions
that cause the problem.
Since each file system primitive makes its own access
checks, there is no guarantee that a file system primitive will
even perform a check. If it does make a check, this check is not
necessarily performed in a similar way to the check performed by
other file system primitives that perform similar functions.
Also, since the auditing and error code censoring functions
are performed by a separate directory control primitive, no
guarantee exists that these functions will be performed.
Add to the above the fact that the basic directory control
primitives are rather cryptic in internal operation. Also, the
logic involved in calling the directory control primitives from
within the various file system primitives is too involved and
complex.
A variety of other bugs exist in the current structuring,
some of which will be discussed later when discussing implica-
tions of the new structuring.
THE NEW STRUCTURING
The new structuring is very simple. The three basic direc-
tory control functions are combined into one module, called
dc_find (directory control find).
dc_find performs the functions of directory
finding/directory entry look up, access checking, security
auditing and error code censoring all in one module.
Structurally, it performs basically the same functions as in the
old structuring. Indeed, find_, find_entry, find_dirsegno,
dir_control_error and uid_path_util (entrypoints $find and
$find_dir) are all internal routines within dc_find. (These
modules are all completely rewritten, of course. The interfaces
to the functions performed by these internal routines are changed
to make them compatible with the overall scheme of things.)
The various entrypoints to dc_find correspond to the differ-
ent types of functions performed within the file system. Some of
the entries locate directories while some locate directory
entries. Each entrypoint has its own set of access checks that
it performs appropriate to the type of file system function on
whose behalf the entrypoint was called. All entrypoints locate
objects in a consistent way, check the access in a consistent
way, audit successful accesses and attempted access violations in |
a consistent way, etc. |
If dc_find determines an attempted access violation, it
returns a null pointer for the directory (or directory entry)
pointer, after having audited the attempted access violation. An
| error code will be returned. If access is allowed, dc_find will
| audit this event and return the desired pointer. The pointer
will always point into a locked directory.
The basic design decision for dc_find is that no entrypoint
should accept as an argument information as to what access check
is to be performed. This information is imbedded in the identity
of the entrypoint called. Each file system primitive should
simply call the dc_find entrypoint that corresponds to the type
of function being performed.
No file system primitive should make its own access check.
| All access checking should be done by dc_find. Likewise, all
| successful accesses and attempted access violations are to be
censored and audited by dc_find. None of the functions performed
by the internal routines within dc_find are to be visible to any
file system primitive so that we can guarantee that the three
basic directory control functions are performed in all cases.
What this means is that all access checks within the file
system can be identified easily (they are all within dc_find).
Also, it is easy to see what access check is performed by a given
file system primitive simply by looking at what dc_find
entrypoints it calls; an analysis of the program's code is not
necessary.
| The access computations within the file system have been
| centralized. Any user ring request for access modes must go
| through dc_find. A new routine has been created for internal
| access computations within directory control. All access compu-
| tations within the file system were examined to determine if any
| violate the new directory control structuring.
| Finally, all entrances to the file system were examined to
| ensure that all entrances that operate upon a file system object
| pass through dc_find for validation.
INTRODUCTION TO THE ENTRIES IN DC_FIND
The entrypoints of dc_find can be grouped into two main
groups. The first group is given the "name" (pathname or uid
path) of a directory. dc_find is to return a pointer to this
directory, having made the appropriate access checks. The second
group is given the "name" (pathname, uid path or pointer) of a
file system object (directory, segment or link). dc_find is to
return a pointer to the directory entry for the object, having
made the appropriate access checks. The first group has
entrypoints whose names start with the prefix "dir_"; the second
group has entrypoints whose names start with the prefix "obj_".
What follows this prefix is some mnemonic describing the function
for which this dc_find entrypoint applies (what file system
function has this type of access check).
Each of the above two groups can be further subdivided. An
entrypoint that expects to be given a uid path has the suffix
"_uid" in its name; an entrypoint that expects a pointer to a
segment has the suffix "_ptr" in its name; an entrypoint having
neither of these suffixes in its name expects an ASCII pathname.
By default, dc_find uses the user's effective access to the
target when checking access. For those (rare) functions that are
based on raw (ACL only) access, the entrypoint name will contain
the suffix "_raw". Privileged functions that don't check access
at all (almost always the target of an hphcs_ entrypoint) have
the suffix "_priv" in the entrypoint name. Note that not all
combinations of these suffixes exist.
If "read" appears in an entrypoint name, the entrypoint is
normally concerned with getting a property. The entrypoint will
return a pointer to a directory (or directory entry) that is
locked for reading. "write" will appear in entrypoint names when
a property is to be set; the directory will be locked for
writing. Corresponding access checks will be made for the "read"
versus "write" entrypoints.
Thus, "dir_read" is the entrypoint called when a file system
primitive wishes to find a pointer to a directory whose contents
are to be read. "dir_write" is called when the contents are to
be modified. If a file system primitive wishes to have a pointer
to a directory entry whose "attribute" properties are to be read
and where a pointer to the target is known,
"obj_attributes_read_ptr" would be the entrypoint used.
The access checks performed by dc_find are the same in all
cases as they were in the old structuring, except for those file
system primitives that incorrectly had no access check before.
CLASSES OF ENTRIES IN DC_FIND
Given the above, the classes of entrypoints within dc_find
follow. The most common entrypoints will be discussed first,
followed by the more obscure (special) entrypoints. The choice
of the various entrypoints (each implying a specific set of
access checks) corresponds to the set of access checks currently
performed within the file system. A later phase of the B2 work
will involve making these more consistent and meaningful.
The various entrypoints are declared in
dc_find_dcls.incl.pl1.
Main Entrypoints
"dir_read", "dir_write" and "dir_salvage" are the major
entrypoints concerned with accessing an entire directory.
"dir_salvage" is obviously used by the directory salvager; it is
special in the manner by which it locks a directory. "dir_read"
and "dir_write" are used when looking at the "contents" of a
directory. "dir_read" requires "s" access on the target directo-
ry; "dir_write" requires "m" access.
"obj_status_read" and "obj_status_write" are used when the
"status" properties of an object are being examined. The "read"
version requires "s" access to the containing directory; the
| "write" version requires "m" access. An exception to this is
| that access related "status" properties (ACL, AIM and ring
| brackets) need to be audited as access modification operations
| instead of as object modification operations. Thus, the
| entrypoint "obj_access_write" exists for changing access
| properties.
"obj_attributes_read" and "obj_attributes_write" are used
when the "attributes" properties of an object are desired. The
read version requires "s" access on the containing directory or
non-null access on the object. The write version requires "m"
access on the containing directory or "w" access (or "m" for a
directory) on the object.
"obj_status_attributes_read" is a hybrid of
"obj_status_read" and "obj_attributes_read" used by the status_
and status_long file system primitives. It is used when both the
"status" and the "attributes" properties are desired and when the
calling primitive can deal with a partial lack of access. It
differs from "obj_attributes_read" when "s" access is missing on
the parent directory. In this case, it returns the directory
entry pointer anyway, audits the partial lack of access, and
returns error_table_$no_s_permission. The calling file system
primitive must honor this error code and not return any "status"
properties to the user ring. This error code must be returned
(or factored into the returned error code) to the user ring.
Specialized Entrypoints
The other entrypoints are each designed for particular file
system functions whose access checks are different from the
above.
The entry "dir_for_append" (used by append) and
"dir_for_retrieve_append" (used by the volume retriever and seg-
ment adopter to append on behalf of someone else) are used to
find a directory into which an object is to be appended. The
access requirement is "a" access on the directory.
"obj_delete" is called when deleting an object. It requires |
"m" access on the parent. It exists basically so that audit |
messages correctly record this particular event. |
"obj_bc_delta_write" (change the bit count) and
"obj_bc_write" (set the bit count) are used when changing/setting
the bit count. Their access check is unusual. The access
requirement for changing/setting the bit count on a segment is
"w" access on the segment. The access requirement for increasing
the bit count on a directory is "a" access on the directory; the
access requirement for decreasing the bit count on a directory is
"m" access on the directory. The directory access requirements
result from an attempt to enforce the model of the bit count on a
directory being the msf component count.
The entries used for the initiate file system function are
"obj_initiate" and "obj_initiate_for_linker_dp". The access |
requirement is non-null access on the target object. The |
"_for_linker_dp" version is used by the dynamic linker search |
facility (fs_search); it accepts a directory pointer and an
entryname instead of a pathname. The "_raw" versions of initiate
specifically allow searching through AIM protected directories,
thus allowing the initiation of any directory or the initiation
of any segment for which the user has any access via the ACL.
The "_raw" version is used by system_privilege_$initiate, |
providing this gate with the ability to violate rings and AIM. |
The opposite of "obj_initiate" is "obj_terminate". This |
entry exists primarily so that the audit message records this |
event. The only access requirement is that dictated by the name |
lookup policy, described later in this MTB. |
"obj_truncate" is used when truncating a segment. This is
considered as affecting a "contents" property. It is special
cased in that this entry does not audit an access violation when
attempting to truncate a segment whose copy switch is on.
(Normally, an attempt to write into a segment to which a process
lacks write permission generates an audited fault. However, if
the copy switch for the segment is on, no audit occurs and the
user ring condition handler replaces, within the address space, a
copy of the segment in the process directory. Thus, if truncate
were purely a user ring function performed by zeroing the ends of
segments, a truncate of a segment to which the process lacks
write permission but whose copy switch is on would properly
truncate the segment. This special casing within hardcore is
respecting this model. However, since hardcore can't truncate
the original, nor does it wish to create the copy for the user,
the user does receive an error, but no audit. This operation is
in keeping with the current system behavior.)
The entrypoint "dir_initiate" is used when bringing a direc- |
tory explicitly into the address space (as opposed to implicitly, |
through directory searches). It is called when the process |
| simply needs to find a directory but in no way operate upon it
| (such as when setting a directory as the working directory). It
exists to enforce the name lookup policy described in a later
section of this MTB.
When quota is to be moved from a parent directory to a son
directory, "dir_move_quota" is used. This operation requires "m"
access on the parent directory and "m" access on the son
directory. To perform this move of quota, the normal access
rules applying to directory modifications are followed in that
the authorization of the process must be equal to the access
class of the parent directory. This entry is special in that it
allows the son directory (but no other directory in the path) to
have an access class that is greater than that of its parent.
"dir_reclassify" is used by the reclassify_node file system
function (reclassify a directory and its contents). It requires
raw "m" access on the parent and raw "sm" access on the target.
"obj_reclassify" is used by the reclassify_seg file system
function. Access requirement is raw "m" on the parent.
The volume retriever uses "obj_volume_retrieve" when actual-
ly retrieving data into a segment or directory. The access
requirements are "rw"/"sm" on the target or "sm" on the parent
directory. Note that this is another access check made on behalf
of another user.
| Segment control's interface to directory control is though
| the "seg_fault" entrypoint. This entrypoint is used only when a
| segment (not a directory) is being connected to the process.
| This entrypoint determines the SDW access control fields. Also,
| this entrypoint audits the resolution of this seg_fault.
| The entrypoint "obj_modes" is used by fs_get when returning
| the process' access modes to an object within the address space.
| The rules are that the process is allowed to know its access
| modes on any object within its address space as long as those
| modes are nonnull or as long as the process has "s" access on the
| parent of the object.
| The get_defname_ function uses "obj_linkage_ring".
| get_defname_ allows the lookup of definitions for any object for
| which the process is within the execute bracket. This entrypoint
| translates a user ring supplied pointer to a segment (one which
| lies within the execute bracket of the object) and translates it
| into a pointer within the read bracket of the object. The user
| must possess effective "e" access to the segment.
The last entrypoints do not return directories or directory
entries. "link_target" is used by get_link_target to chase links
and find the identity of the target (existent or not).
Finally, "finished" is provided to help clean up after calls *
to dc_find.
IMPROVEMENTS
As mentioned above, the get_link_target function is fixed.
(That is, it will return the pathname of the target of a link
even if the target is non-existent.)
The security policy allowed user's to read "attribute"
properties when they lacked "s" access on the parent but had
non-null access to an object. Due to a bug, this did not work
when the user had null access on both the parent and the parent's
parent. This has been fixed. As a result, the problem with
hcs_$get_ring_brackets that is preventing installation of the
message segment/mailbox software goes away.
The performance of directory finding has been improved.
This is because the function was converted from a recursive one
to an iterative one (fixing a fatal process error problem
occurring in certain link chasing scenarios). Other (minor)
performance improvements are expected now that the three basic
directory control functions are all performed in one module, and
performed by "quick" blocks at that.
dc_find has paths through it to support access checking and |
auditing on behalf of another user. (This is used currently only
by the volume retriever and the segment adopter.) As such, |
auditable events requested by another user will now be audited. |
A fix has been made to pathname associative memory opera-
tions related to upgraded directories.
NEW ERROR CODE CENSORING POLICY
A portion of Multics' access control policy is being
changed. This has a variety of implications.
Name Lookup Policy
The portion of the access control policy being changed is
that which deals with the situation where the file system
function a user requests is not performed. There are two reasons
(relative to this discussion) why a function that is requested is
not done. The first is that the user lacks sufficient access to
perform the function. The second is that the target doesn't
exist.
The old policy stated (implicitly) that the existence or
non-existence of an object was not privileged information, but
that the access existing on an object was privileged information
(this latter part is still true). As such, an attempt to access
a non-existent object always (except for crossing directory AIM
boundaries) returned error_table_$noentry, regardless of whether
the user had access (by some measure) to know of the
non-existence of the object. In those cases when the object did
exist but the user lacked access, the error message the user was
allowed to see depended on the user's ability to see the user's
access. This depended on the user's access to the parent's
parent.
The new policy states that the existence or non-existence of
an object is privileged information in as much as that the list
of names that appear in a directory (and the (nearly infinite)
list of names that do not appear in a directory) are access
controlled information. A user is allowed to see the existence
of an object in a directory (that is, to look up the name) only
if the user has access to the object (which allows the user to
operate on it) or if the user has non-null access on the
directory that would hold the object. (Having "s" access on the
parent directory allows the user to explicitly see the object;
"m" allows operating upon it; "a" allows attempting to append
another occurrence of the name, allowing a test for
error_table_$namedup.) This new policy is a more restrictive
policy that supersedes the old policy.
The difference between the old policy and the new appears in
basically two places. First, when the user attempts a file
system function on an object, has insufficient access to perform
it, has null access on the parent but non-null access on the
parent's parent, the old policy would have returned
error_table_$incorrect_access ("Incorrect access to directory
containing entry."). The new policy will return
error_table_$no_info ("Insufficient access to return any informa-
tion."). Second, when the user refers to a non-existent object
in a directory in which the user has null access, the old policy
would have returned error_table_$noentry whereas the new policy
will return error_table_$no_info.
The single case in which error_table_$incorrect_access will
be replaced by error_table_$no_info is not expected to be notice-
able to anyone. However, the case where error_table_$noentry is
replaced by error_table_$no_info is expected to be more notice-
able. In particular, user ring programs before could depend on
the presence of error_table_$noentry to provide the indication of
the non-existence of an object. It is no longer possible to be
assured of the non-existence of an object.
Implications
A subtle change occurs in the case of append. For the new
policy to be handled consistently, the policy, when applied to
append, states that attempting to append an object within a
directory corresponds to attempting to ask the existence of the
object within the directory. Thus, attempting to append an
object in a directory to which the user has null access returns
error_table_$no_info, whereas the old policy might have returned
error_table_$incorrect_access or error_table_$namedup.
Attempting to look up a name in a directory is an access
controllable function. We must audit failures to look up a name.
Failures to look up a name include: finding that a desired
object doesn't exist; finding that a directory in a pathname
doesn't exist; finding that a directory in a pathname is really a
segment; finding that the target of an append function does
exist; finding that the link target of a set of links is
non-existent but the user lacks access in what would be the final
target directory to see the non-existence. Since failures to
look up a name occur reasonably often (the user mis-types a file
name, the user tries to create something already existing, etc.),
this would have a severe effect. So, we only audit attempts to
look up a name that fail in those cases where the user is not
allowed to know if the name exists or not (those in which we
return error_table_$no_info).
A few file system functions are restricted by this name
lookup policy. For example, terminate_file now only allows the
termination of a segment by pathname for those segments that are
visible by this policy. The other functions restricted by this
policy are listed under "Directory Lookup" below.
Dynamic Linker Interface
Even this simple rule about auditing name lookups has an
exception, though. In normal system operation, we expect to have
name lookup failures occurring rarely in comparison to those
cases where the name lookup succeeds. The failures would be in
the error paths of operations. As such, we are relatively
uninterested in the decrease in performance resulting from the
check to see if we should return error_table_$no_info. There is,
however, a place in the system that expects to generate name
lookup failures: the dynamic linker. We could not tolerate a
performance loss there. However, the dynamic linker's access to
the three basic directory control functions (from within
fs_search) is to initiate$initiate_seg_count (for which it is the
only caller) which calls dc_find$initiate_for_linker_dp (for |
which it is the only caller). Because of this, an arrangement |
has been made between dc_find$initiate_for_linker_dp and its |
caller. This one entrypoint does not make a check when a name |
failure occurs to see if error_table_$no_info should be returned.
Instead, it always returns error_table_$no_info (unless it suc-
ceeds in finding the object and the access checks pass, of
course). This event is not audited. This has no security
implications since we are passing out less information than we
would if we made the check, not more. Also, the dynamic linker
doesn't care (except for cases of error_table_$moderr) why it
couldn't initiate an object, so it doesn't care about this lack
of information.
Directory Lookup
Consider the case where a directory in a supplied pathname
doesn't exist. The old policy mapped the error_table_$noentry
returned in looking for the named directory into
error_table_$no_dir ("Some directory in path specified does not
exist."). The new policy will return error_table_$no_dir only
when the user has access to see that the directory doesn't exist;
otherwise the user will get error_table_$no_info. This may
surprise some people.
Also consider the case of setting your working directory.
The old policy would allow you to change your working directory
to any directory (except for crossing AIM boundaries). However,
this action would tell you whether the target existed or not, and
whether it was a directory or not. This cannot be allowed. Some
access check is required. A reasonable, but too restrictive one,
would be that you must have "s" access on the directory being
set. A less restrictive check, the one that was chosen, is to
allow a user to set the working directory to any directory that
the user is allowed to see (non-null access on the directory or
non-null access on its parent). This is simply the name lookup
error censoring policy from above.
This access check on the target of a set working directory
function also extends to adding a directory to your search rules.
It also applies to setting your home directory (such as on the
login line). Setting the home directory to one which fails this
check cannot be checked by the Initializer. Instead, your home
directory (as a character string) will be set to the given value,
whether the directory exists or not. However, an attempt to set
your working directory to this value at process start up will
fail.
| SUCCESSFUL ACCESS AUDITING
| As a part of fulfilling the B2 requirements, directory
| control will have the capability of providing an audit trail of
| all successful accesses to file system objects. This capability
| is being implemented within dc_find.
| dc_find is the obvious place for successful access auditing.
| Since all requests placed upon the file system must pass through
| dc_find, and dc_find determines the validity of all such
| requests, it follows that dc_find forms a centralized point that
| is aware of all file system operations being attempted. By
having dc_find perform the successful access auditing, we are |
assured that all file system operations will be properly audited. |
When dc_find determines that a file system request is valid, |
it will perform the necessary functions for auditing before |
returning the desired entry or directory pointers. It will |
perform these operations with the directories locked, since entry |
pointers are needed by the auditing module. dc_find will call |
access_audit_$check_XXX and access_audit_$log_XXX in a |
centralized place within itself. The audit message will always |
refer to the intended target of the operation desired, not the |
parent or the parent's parent, etc. Note that access_audit_ will |
also be used, within this same centralized place, to generate the |
audit messages for attempted access violations. These messages |
will now also refer to the target only. |
Auditable Events |
The list of auditable events, along with the degree to which |
they are identified in the audit message, is given below. As a |
reminder, each audited event includes an encoded access opera- |
tion. The encoded operation, for file system objects, specifies |
whether the event is a read, modify or modify_access operation |
and whether it applies to the "contents" of the object or to the |
attributes ("status" or "attribute" properties) of the object. |
An operation code field shows the type of operation (object |
creation, object reading, etc.). For modify or modify_access |
operations, the detailed operation field in the encoded access |
operation provides the details of the properties that were |
modified. |
OBJECT CREATION |
The creation of a file system object (directory, entry or |
link) is audited distinctly as an object creation. This event |
forms the single exception to the above rule stating that all |
audit messages refer to the target of the operation. This |
exception exists because at the time that append access is |
granted by dc_find, the object does not yet exist. So, the audit |
message will refer to the directory into which the object is |
being appended, plus a comment giving the name of the new object. |
The module "append" itself will later add an audit message that |
refers to this new object, so that its uid, etc. are recorded. |
(One might say that the above exception suggests that |
auditing of successful accesses should be performed by the file |
system primitives once they complete an operation. However, this |
decentralizes the auditing support, which is undesirable. Also, |
since a file system primitive may start an operation, but not |
finish it (and therefore omit the audit message), it is best to |
audit when it is decided that a file system operation is being |
| started (access was granted). Even if we didn't audit until
| operations were completed, then object deletion would be stuck
| trying to generate an audit message to a now non-existent object.
| So, we are stuck having an exception somewhere; append is it.)
| So, the creation of an object generates two audits. The
| first audit has an operation code of fs_obj_contents_mod (modifi-
| cation of the contents of an object), describing the modification
| of the parent to show the creation of the directory entry. The
| detail field shows that this is an object creation. The second
| audit has an operation code of fs_obj_create (modification of the
| attributes of the object).
| OBJECT DELETION
| The deletion of a file system object is audited with an
| operation code of fs_obj_delete (modification of the attributes
| of the object).
| OBJECT INITIATION
| The addition of a segment to the address space by user
| direction is considered as an object initiation. Also, the
| addition of a directory by command, as the current working
| directory or as a directory within the search rules, is also
| considered as a object initiation. These events are audited with
| the operation code fs_obj_initiate (reading of the attributes of
| the object). The implicit addition of a directory into the
| address space when locating an object is not audited, except for
| violations of the name lookup policy.
| OBJECT TERMINATION
| The explicit termination of a segment from the address space
| is audited with an operation code of fs_obj_terminate (reading
| the attributes of an object).
| SEGMENT CONTENTS REFERENCES
| References to the contents of a segment are detected by
| seg_fault. The access maintenance mechanism within hardcore has
| been revised so that seg_fault (actually dc_find$seg_fault) is
| the only keeper and updater of sdw access fields. An audit will
| occur at seg_fault time only if the access being granted is
| different from the previously granted access. This will avoid
| multiple audits as a segment is promoted through aste pools, or
| multiply activated and deactivated. Note that the first
| seg_fault to a segment will always audit. The audit will have
| the operation code of fs_obj_contents_read unless the granted
access includes write permission; in this case the code is |
fs_obj_contents_mod. |
DIRECTORY CONTENTS REFERENCES |
References to read the contents of a directory (IACLS, name |
list, quota) will be audited with the fs_obj_contents_read code. |
Modifications of these properties will have the code of |
fs_obj_contents_mod, with a detail field providing the list of |
properties modified. |
READING OBJECT PROPERTIES |
Attempts to read any arbitrary set of properties of an |
object will be audited with the fs_obj_prop_read code. No |
special code exists to distinguish the reading of "status" versus |
"attribute" properties. |
MODIFYING OBJECT PROPERTIES |
The modification of object properties is broken down for |
greater resolution. The operation codes are fs_obj_attr_mod (for |
"attribute" properties), fs_obj_status_mod (for "status" |
properties) and fs_obj_access_mod (for modifications of access |
related properties (ACL, AIM and rings)). The detail field names |
the set of properties modified. |
PROBLEMS AND FUTURE WORK
More work can always be done. As the file system primitives
are being updated to call the new directory control primitives,
they are being neatened in some minor ways (arranging declara-
tions, formatting, etc.). It would be desirable, though, to
really straighten them up; that is, make their variable names
meaningful, make them more structured, etc. I hope to do some of
this if I get a chance.
Towards a Security Kernel
Although I don't intend to suggest that we attempt to really
create a "security kernel" for Multics, there are some things we
can do. This directory control restructuring is a first step. |
The modules that previously constituted the hardcore bound |
segments of bound_file_system, bound_priv_procs and |
bound_system_faults were rearranged to form bound_dir_control, |
bound_file_system and bound_segment_control. bound_dir_control |
contains the security related portions of the file system (as |
| well as the primitives that directly operate upon directory
| structures). bound_file_system contains the file system primi-
| tives. Some day we may break this down further, when we have
| support for restricting which bound segments can operate upon
| security related objects.
Towards a Consistent Access Model
dc_find currently concerns itself only with the access
checks described above. In particular, it only concerns itself
with access checks that are currently considered to be auditable.
These checks are not the only checks made, though. When setting
the safety switch, for example, the checks that dc_find make ("m"
access required on the containing directory) are followed by a
check (within the file system primitive set) to be sure that the
user's validation level is within the write bracket of the
object. (Note that the user need not have any specific access to
the object (via the ACL); the user only need be within the write
bracket of the object. This validation level check is not to be
confused with the factoring in of the validation level when
computing the user's effective access to the containing directory
when making the "m" access check within dc_find.) If the
validation level is not within the write bracket, this is
currently considered to be a simple argument style error, the
user gets error_table_$bad_ring_brackets, and no audit occurs. A
question exists as to whether this should be audited. Aside from
whether it should be audited, though, having this check external
to dc_find, again, means that it may not be applied uniformly.
Currently, this type of check is applied very non-uniformly. As
such, until a more consistent access model is made, it is not
possible to consider moving such checks into dc_find.
This brings up the subject of creating a consistent access
model. This would be a good idea but it would be hard to migrate
the software to it. A consistent access model would be one that
had uniform policy statements that applied to the validation
level check, made above. It would also create a consistent view
of the classes of properties (however many there would have to be
to be consistent) and the accesses required to manipulate them.
Consider the case of the bit count. When setting the bit
count, it is considered a "contents" property. When reading,
though, it is considered an "attributes" property. This is
inconsistent. To make it consistent, we would have to make
reading the bit count be a "contents" property access; that is,
| it would require nonnull access on the object itself to read it.
This sounds like an easy, harmless change. However, it would
affect status_$minf (as well as probably others). status_$minf
returns the type of an object, as well as its bit count. The
type of an object is an "attributes" property; the bit count is
properly a "contents" property. status_$minf, then, becomes a
mixed property type function just like status_$status_long. That
is, different access checks would apply to the two different
return values. If the user had "s" access on an object's
containing directory but did not have "r" access (or "s") on the
object, the user would be allowed to see the type but not the bit
count. The user would be given error_table_$moderr (and this
audited). There are countless user ring programs that would not
cope with this error code. Thus, it would be hard to change to
this consistency. Also, this change would require that the star_ |
function used by the list command would have to check for nonnull |
access on every object it lists. This would have undesirable |
performance implications. |
Error Code Mis-filtering
Another problem that exists is that various system commands
(and I must imagine, user programs) know the set of error codes
that used to be returned and exactly when they were returned.
The old directory control structuring returned a somewhat
inconsistent set of codes. Various commands knew that, and
filtered the codes to suit the author's own beliefs of what were
more meaningful error messages. Now that the error codes have
been made consistent (relative to name lookup and access ques-
tions), some commands may mis-interpret the returned codes and
end up confusing users. A pass needs to be made sometime for
commands that attempt to interpret their own meaning into
hardcore's returned error codes.
Problems with Names
There is a place in the system in which we do not properly
enforce access control on the names of an object. When a user
succeeds in initiating an object, the name by which the user
initiated the object is clearly information the user is allowed
to know. Even if the user does not have "s" access on the
containing directory (normally needed to see the names of an
object), the user is allowed to know this name (the user must
have been told this name by someone). The problem is that this
pathname is not remembered (except in the pathname associative
memory and possibly as a reference name on the object); only the
fact that this object exists in the address space is remembered.
If the user asks for the pathname of this object at a later time,
the user may be given the primary name (and possibly the primary
names of some of the directories in the pathname, depending on
the state of the pathname associative memory). In this way, we
are not enforcing access control in that we are giving the user a
name (the primary name) which the user did not necessarily know
before. We currently are saying that the primary name of an
object is an "attributes" property.
The last existing problem I will cover is that of
directories appearing in the user's address space. This problem
was discussed before in the literature so I won't discuss it in
great depth. The problem is that, to walk down a set of
directories to find an object, these directories must be brought
into (and stay within) the user's address space. This occurs
even if the user lacks access to the directories. If the user
does possess access to the target, it is okay for these
directories to appear in the user's address space since the user
clearly knows that they exist (by virtue of the user's knowing
the existence of the final target). However, if the user does
not possess access, dc_find will not bring the target into the
address space, thereby not informing the user of the possible
existence of the target; but, the directories in the path will
come into the address space. It is virtually impossible to
imagine a scheme that would perfectly clean these up, or that
would prevent the user from setting up the environment so as to
be able to sense how many directories came into the address
space. So, we are stuck with no way to keep the user from
testing the existence of directories. This is one of the reasons
why we now audit failures to look up names; it helps catch
someone probing for the names of directories.
| An attempt was made some time ago to implement a scheme that
| would hide these directories from the user. Part of the plan
| involved the notion of detectable versus undetectable segments.
| A segment was to be detectable in only certain rings (those not
| greater than the segment's highest detectable ring (hdr)). In
| this scheme, it was possible for a directory to appear in the
| address space more than once to hide its detectability. This
| scheme was never fully implemented (and would not have completely
| solved the problem). The notion of hdr is being deleted in this
| installation, as well as the notion of segment detectability.
SUMMARY
The new dc_find directory control primitive is the start of
improving the hardcore interfaces to users and processes. It
provides our first attempt at creating a consistent access
control model and providing a way that allows us to feel
confident that this model is being enforced. However, the reader
should stay tuned for the announcement of more work to be done.