Skip to content

Commit c8b3bc4

Browse files
ve1nardlind
andcommitted
rmdir_syscall() updates (#289)
* added a test for search permissions bug * updates syscall description * finished rmdir_syscall * bug fix * test changes * fixed everything * updated comments * fixed according to the review * performed cargo fmt * added a comment for close_helper_inner --------- Co-authored-by: lind <lind@nyu.edu>
1 parent 9ec2078 commit c8b3bc4

File tree

3 files changed

+313
-18
lines changed

3 files changed

+313
-18
lines changed

src/safeposix/syscalls/fs_calls.rs

Lines changed: 126 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2607,9 +2607,15 @@ impl Cage {
26072607
None => {}
26082608
}
26092609
if dir_inode_obj.linkcount == 2 && dir_inode_obj.refcount == 0 {
2610-
//removing the file from the metadata
2611-
FS_METADATA.inodetable.remove(&inodenum);
2610+
//The reference to the inode has to be dropped to avoid
2611+
//deadlocking because the remove() method will need to
2612+
//acquire a reference to the same inode from the
2613+
//filesystem's inodetable.
2614+
//The inodetable represents a Rust DashMap that deadlocks
2615+
//when trying to get a reference to its entry while holding any sort
2616+
//of reference into it.
26122617
drop(inodeobj);
2618+
FS_METADATA.inodetable.remove(&inodenum);
26132619
log_metadata(&FS_METADATA, inodenum);
26142620
}
26152621
}
@@ -3382,64 +3388,169 @@ impl Cage {
33823388
0
33833389
}
33843390

3385-
//------------------RMDIR SYSCALL------------------
3391+
/// ### Description
3392+
///
3393+
/// The `rmdir_syscall()` deletes a directory whose name is given by `path`.
3394+
/// The directory shall be removed only if it is an empty directory.
3395+
///
3396+
/// ### Arguments
3397+
///
3398+
/// The `rmdir_syscall()` accepts one argument:
3399+
/// * `path` - the path to the directory that shall be removed. It can be
3400+
/// either
3401+
/// relative or absolute (symlinks are not supported).
3402+
///
3403+
/// ### Returns
3404+
///
3405+
/// Upon successful completion, 0 is returned.
3406+
/// In case of a failure, an error is returned, and `errno` is set depending
3407+
/// on the error, e.g. EACCES, ENOENT, etc.
3408+
///
3409+
/// ### Errors
3410+
///
3411+
/// * `ENOENT` - `path` is an empty string or names a nonexistent directory
3412+
/// * `EBUSY` - `path` names a root directory that cannot be removed
3413+
/// * `ENOEMPTY` - `path` names a non-empty directory,
3414+
/// * `EPERM` - the directory to be removed or its parent directory
3415+
/// does not allow write permission
3416+
/// * `ENOTDIR` - `path` is not a directory
3417+
/// Other errors, like `EACCES`, `EINVAL`, etc. are not supported.
3418+
///
3419+
/// ### Panics
3420+
/// A panic occurs when the directory to be removed does not have `S_IFDIR"`
3421+
/// (directory file type flag) set or when parent inode is not a directory.
3422+
///
3423+
/// To learn more about the syscall, error values, etc.,
3424+
/// see [rmdir(3)](https://linux.die.net/man/3/rmdir)
33863425
33873426
pub fn rmdir_syscall(&self, path: &str) -> i32 {
33883427
if path.len() == 0 {
3389-
return syscall_error(Errno::ENOENT, "rmdir", "Given path is null");
3428+
return syscall_error(Errno::ENOENT, "rmdir", "Given path is an empty string");
33903429
}
3430+
//Convert the provided pathname into an absolute path without `.` or `..`
3431+
//components.
33913432
let truepath = normpath(convpath(path), self);
33923433

3393-
// try to get inodenum of input path and its parent
3434+
//Perfrom a walk down the file tree starting from the root directory to
3435+
//obtain an inode number of the file whose pathname was specified and
3436+
//its parent directory's inode.
33943437
match metawalkandparent(truepath.as_path()) {
33953438
(None, ..) => syscall_error(Errno::ENOENT, "rmdir", "Path does not exist"),
3396-
(Some(_), None) => {
3397-
// path exists but parent does not => path is root dir
3398-
syscall_error(Errno::EBUSY, "rmdir", "Cannot remove root directory")
3399-
}
3439+
//The specified directory exists, but its parent does not,
3440+
//which means it is a root directory that cannot be removed
3441+
(Some(_), None) => syscall_error(Errno::EBUSY, "rmdir", "Cannot remove root directory"),
34003442
(Some(inodenum), Some(parent_inodenum)) => {
3443+
//If the parent directory of the directory that shall be removed
3444+
//doesn't allow write permission, the removal cannot be performed
3445+
if let Inode::Dir(ref mut parent_dir) =
3446+
*(FS_METADATA.inodetable.get_mut(&parent_inodenum).unwrap())
3447+
{
3448+
// check if parent directory has write permissions
3449+
if parent_dir.mode as u32 & (S_IWOTH | S_IWGRP | S_IWUSR) == 0 {
3450+
return syscall_error(
3451+
Errno::EPERM,
3452+
"rmdir",
3453+
"Parent directory does not have write permission",
3454+
);
3455+
}
3456+
}
3457+
//Getting a mutable reference to an inode struct that corresponds to
3458+
//the directory that shall be removed
34013459
let mut inodeobj = FS_METADATA.inodetable.get_mut(&inodenum).unwrap();
34023460

34033461
match &mut *inodeobj {
3404-
// make sure inode matches a directory
3462+
//A sanity check to make sure the inode matches a directory
34053463
Inode::Dir(ref mut dir_obj) => {
3464+
//When a new empty directory is created, its linkcount
3465+
//is set to 3. Thus, any empty directory should have a
3466+
//linkcount of 3. Otherwise, it is a non-empty directory
3467+
//that cannot be removed
34063468
if dir_obj.linkcount > 3 {
34073469
return syscall_error(
34083470
Errno::ENOTEMPTY,
34093471
"rmdir",
34103472
"Directory is not empty",
34113473
);
34123474
}
3475+
//A sanity check to make sure that the correct directory
3476+
//file type flag is set
34133477
if !is_dir(dir_obj.mode) {
34143478
panic!("This directory does not have its mode set to S_IFDIR");
34153479
}
34163480

3417-
// check if dir has write permission
3481+
//The directory to be removed should allow write permission
34183482
if dir_obj.mode as u32 & (S_IWOTH | S_IWGRP | S_IWUSR) == 0 {
34193483
return syscall_error(
34203484
Errno::EPERM,
34213485
"rmdir",
3422-
"Directory does not have write permission",
3486+
"Directory does not allow write permission",
34233487
);
34243488
}
34253489

3490+
//The directory cannot be removed if one or more processes
3491+
//have the directory open, which corresponds to a non-zero
3492+
//reference count
34263493
let remove_inode = dir_obj.refcount == 0;
3494+
//Any new empty directory has a linkcount of 3.
3495+
//If the refcount of the directory is 0, it means
3496+
//that there are no open file descriptors for that directory,
3497+
//and it can be safely removed both from the filesystem
3498+
//and the parent directory's inode table.
3499+
//However, if there exists an open file descriptor for that
3500+
//directory, it cannot be removed from the filesystem because
3501+
//otherwise, the process that has the directory open
3502+
//will end up calling `close_syscall()` on a nonexistent directory.
3503+
//At the same time, after `rmdir_syscall()` is called on the directory,
3504+
//no new files can be created inside it, even if the directory is open
3505+
//by some process. To prevent creating new files inside the directory,
3506+
//we delete its entry from the parent directory's inode table.
3507+
//This way, in case there is an open file descriptor for the directory
3508+
//to be removed, by deleting the directory's entry from its parent
3509+
//directory's inode table and keeping the directory's entry in the
3510+
//filesystem table, we both disallow the creation of any files inside
3511+
//that directory and prevent the process that has the directory
3512+
//open from closing a nonexistent directory.
3513+
//Setting the directory's linkcount as 2 works as a flag for
3514+
//the `close_syscall()` to mark the directory that needs to be
3515+
//removed from the filesystem when its last open file descriptor
3516+
//is closed because it could not be removed at the time of calling
3517+
//`rmdir_syscall()` because of some open file descriptor.
34273518
if remove_inode {
34283519
dir_obj.linkcount = 2;
3429-
} // linkcount for an empty directory after rmdir must be 2
3520+
}
3521+
3522+
//The mutable reference to the inode has to be dropped because
3523+
//remove_from_parent_dir() method will need to acquire an immutable
3524+
//reference to the parent directory's inode from the filesystem's
3525+
//inodetable. The inodetable represents a Rust DashMap that deadlocks
3526+
//when trying to get a reference to its entry while holding any sort
3527+
//of reference into it.
34303528
drop(inodeobj);
34313529

3530+
//`remove_from_parent_dir()` helper function returns 0 if an
3531+
//entry corresponding to the specified directory was
3532+
//successfully removed from the filename-inode dictionary
3533+
//of its parent.
3534+
//If the parent directory does not allow write permission,
3535+
//`EPERM` is returned.
3536+
//As a sanity check, if the parent inode specifies a
3537+
//non-directory type, the funciton panics
34323538
let removal_result =
34333539
Self::remove_from_parent_dir(parent_inodenum, &truepath);
34343540
if removal_result != 0 {
34353541
return removal_result;
34363542
}
34373543

3438-
// remove entry of corresponding inodenum from inodetable
3544+
//Remove entry of corresponding inodenum from the filesystem
3545+
//inodetable
34393546
if remove_inode {
34403547
FS_METADATA.inodetable.remove(&inodenum).unwrap();
34413548
}
3442-
3549+
//Log is used to store all the changes made to the filesystem. After
3550+
//the cage is closed, all the collected changes are serialized and
3551+
//the state of the underlying filesystem is persisted. This allows us
3552+
//to avoid serializing and persisting filesystem state after every
3553+
//`rmdir_syscall()`.
34433554
log_metadata(&FS_METADATA, parent_inodenum);
34443555
log_metadata(&FS_METADATA, inodenum);
34453556
0 // success

src/tests/fs_tests.rs

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1547,15 +1547,190 @@ pub mod fs_tests {
15471547
}
15481548

15491549
#[test]
1550-
pub fn ut_lind_fs_rmdir() {
1550+
pub fn ut_lind_fs_rmdir_normal() {
1551+
//acquiring a lock on TESTMUTEX prevents other tests from running concurrently,
1552+
//and also performs clean env setup
1553+
let _thelock = setup::lock_and_init();
1554+
1555+
let cage = interface::cagetable_getref(1);
1556+
1557+
//We create a new parent directory `/parent_dir`
1558+
//and its child directory '/parent_dir/dir` both
1559+
//with the required write permission flags, thus
1560+
//calling `rmdir_syscall()`on the child directory
1561+
//should result in a normal behavior
1562+
let path = "/parent_dir/dir";
1563+
assert_eq!(cage.mkdir_syscall("/parent_dir", S_IRWXA), 0);
1564+
assert_eq!(cage.mkdir_syscall(path, S_IRWXA), 0);
1565+
assert_eq!(cage.rmdir_syscall(path), 0);
1566+
//To check if the child directory was successfully
1567+
//removed, we call `open_syscall()` on it, and see
1568+
//if it correctly returns `Path does not exist` error
1569+
assert_eq!(
1570+
cage.open_syscall(path, O_TRUNC, S_IRWXA),
1571+
-(Errno::ENOENT as i32)
1572+
);
1573+
1574+
assert_eq!(cage.exit_syscall(EXIT_SUCCESS), EXIT_SUCCESS);
1575+
lindrustfinalize();
1576+
}
1577+
1578+
#[test]
1579+
pub fn ut_lind_fs_rmdir_empty_path() {
15511580
//acquiring a lock on TESTMUTEX prevents other tests from running concurrently,
15521581
// and also performs clean env setup
15531582
let _thelock = setup::lock_and_init();
15541583

15551584
let cage = interface::cagetable_getref(1);
15561585

1586+
//Trying to remove a directory by providing an empty string
1587+
//should return `Given path is an empty string` error
1588+
assert_eq!(cage.rmdir_syscall(""), -(Errno::ENOENT as i32));
1589+
1590+
assert_eq!(cage.exit_syscall(EXIT_SUCCESS), EXIT_SUCCESS);
1591+
lindrustfinalize();
1592+
}
1593+
1594+
#[test]
1595+
pub fn ut_lind_fs_rmdir_nonexist_dir() {
1596+
//acquiring a lock on TESTMUTEX prevents other tests from running concurrently,
1597+
// and also performs clean env setup
1598+
let _thelock = setup::lock_and_init();
1599+
1600+
let cage = interface::cagetable_getref(1);
1601+
1602+
//We create a new parent directory `/parent_dir`
1603+
//However, we never create its child directory
1604+
//'/parent_dir/dir`, thus calling `rmdir_syscall()`
1605+
//on this child directory should return
1606+
//`Path does not exist` error
15571607
let path = "/parent_dir/dir";
15581608
assert_eq!(cage.mkdir_syscall("/parent_dir", S_IRWXA), 0);
1609+
assert_eq!(cage.rmdir_syscall(path), -(Errno::ENOENT as i32));
1610+
1611+
assert_eq!(cage.exit_syscall(EXIT_SUCCESS), EXIT_SUCCESS);
1612+
lindrustfinalize();
1613+
}
1614+
1615+
#[test]
1616+
pub fn ut_lind_fs_rmdir_root() {
1617+
//acquiring a lock on TESTMUTEX prevents other tests from running concurrently,
1618+
// and also performs clean env setup
1619+
let _thelock = setup::lock_and_init();
1620+
1621+
let cage = interface::cagetable_getref(1);
1622+
1623+
//Trying to remove the root directory should return
1624+
//`Cannot remove root directory` error
1625+
assert_eq!(cage.rmdir_syscall("/"), -(Errno::EBUSY as i32));
1626+
1627+
assert_eq!(cage.exit_syscall(EXIT_SUCCESS), EXIT_SUCCESS);
1628+
lindrustfinalize();
1629+
}
1630+
1631+
#[test]
1632+
pub fn ut_lind_fs_rmdir_nonempty_dir() {
1633+
//acquiring a lock on TESTMUTEX prevents other tests from running concurrently,
1634+
//and also performs clean env setup
1635+
let _thelock = setup::lock_and_init();
1636+
1637+
let cage = interface::cagetable_getref(1);
1638+
1639+
//We create a new parent directory `/parent_dir` and
1640+
//its child directory '/parent_dir/dir`, thus calling `rmdir_syscall()`
1641+
//on the parent directory should return `Directory is not empty` error
1642+
let path = "/parent_dir/dir";
1643+
assert_eq!(cage.mkdir_syscall("/parent_dir", S_IRWXA), 0);
1644+
assert_eq!(cage.mkdir_syscall(path, S_IRWXA), 0);
1645+
assert_eq!(
1646+
cage.rmdir_syscall("/parent_dir"),
1647+
-(Errno::ENOTEMPTY as i32)
1648+
);
1649+
1650+
assert_eq!(cage.exit_syscall(EXIT_SUCCESS), EXIT_SUCCESS);
1651+
lindrustfinalize();
1652+
}
1653+
1654+
#[test]
1655+
pub fn ut_lind_fs_rmdir_nowriteperm_child_dir() {
1656+
//acquiring a lock on TESTMUTEX prevents other tests from running concurrently,
1657+
// and also performs clean env setup
1658+
let _thelock = setup::lock_and_init();
1659+
1660+
let cage = interface::cagetable_getref(1);
1661+
1662+
//We create a new parent directory `/parent_dir` with all write permission
1663+
//flags and its child directory '/parent_dir/dir` without any write
1664+
//permision flags, thus calling `rmdir_syscall()`on the child directory
1665+
//should return `Directory does not allow write permission` error
1666+
//because the directory cannot be removed if it does not allow
1667+
//write permission
1668+
let path = "/parent_dir/dir";
1669+
assert_eq!(cage.mkdir_syscall("/parent_dir", S_IRWXA), 0);
1670+
assert_eq!(cage.mkdir_syscall(path, 0), 0);
1671+
assert_eq!(cage.rmdir_syscall(path), -(Errno::EPERM as i32));
1672+
1673+
assert_eq!(cage.exit_syscall(EXIT_SUCCESS), EXIT_SUCCESS);
1674+
lindrustfinalize();
1675+
}
1676+
1677+
#[test]
1678+
pub fn ut_lind_fs_rmdir_nowriteperm_parent_dir() {
1679+
//acquiring a lock on TESTMUTEX prevents other tests from running concurrently,
1680+
// and also performs clean env setup
1681+
let _thelock = setup::lock_and_init();
1682+
1683+
let cage = interface::cagetable_getref(1);
1684+
1685+
//We create a new parent directory `/parent_dir` with all write permission
1686+
//flags (to be able to create its child directory) and its child directory
1687+
//'/parent_dir/dir` with all write permision flags.
1688+
let path = "/parent_dir/dir";
1689+
assert_eq!(cage.mkdir_syscall("/parent_dir", S_IRWXA), 0);
1690+
assert_eq!(cage.mkdir_syscall(path, S_IRWXA), 0);
1691+
//Now, we change the parent directories write permission flags to 0,
1692+
//thus calling `rmdir_syscall()`on the child directory
1693+
//should return `Directory does not allow write permission` error
1694+
//because the directory cannot be removed if its parent directory
1695+
//does not allow write permission
1696+
assert_eq!(
1697+
cage.chmod_syscall("/parent_dir", S_IRUSR | S_IRGRP | S_IROTH),
1698+
0
1699+
);
1700+
assert_eq!(cage.rmdir_syscall(path), -(Errno::EPERM as i32));
1701+
1702+
assert_eq!(cage.exit_syscall(EXIT_SUCCESS), EXIT_SUCCESS);
1703+
lindrustfinalize();
1704+
}
1705+
1706+
#[test]
1707+
//BUG:
1708+
//The correct behavior of the `rmdir_syscall()` when called on a directory
1709+
//whose path includes a component that does not allow search permission
1710+
//(the read flag) is to return with `EACCES` error.
1711+
//However, the `metawalkandparent())` helper function used
1712+
//to retrieve the inodes of the directory to be removed and its parent
1713+
//directory does not check for search permission. Thus, the following test
1714+
//will not return any errors and run normally even though the parent
1715+
//directory does not grand search permission.
1716+
pub fn ut_lind_fs_search_permission_bug_with_rmdir() {
1717+
//acquiring a lock on TESTMUTEX prevents other tests from running concurrently,
1718+
//and also performs clean env setup
1719+
let _thelock = setup::lock_and_init();
1720+
1721+
let cage = interface::cagetable_getref(1);
1722+
1723+
//Creating the parent directory that does not allow search permission
1724+
//by excluding any read flags and specifying only write flags
1725+
//to be able to delete the child directory.
1726+
let path = "/parent_dir/dir";
1727+
assert_eq!(
1728+
cage.mkdir_syscall("/parent_dir", S_IWUSR | S_IWGRP | S_IWOTH),
1729+
0
1730+
);
1731+
//Creating the child directory with all the required flags
1732+
//and then deleting it. Because of the bug described above,
1733+
//removing the directory will not return any errors.
15591734
assert_eq!(cage.mkdir_syscall(path, S_IRWXA), 0);
15601735
assert_eq!(cage.rmdir_syscall(path), 0);
15611736

0 commit comments

Comments
 (0)