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.