Support setting file mode

File mode of writable AuthFsEntry can now be changed. The mode is
maintain privately in authfs, but also pass through to fd_server.

Note that this change only aims to support getting/setting the file
mode. The mode is not currently used for ACL check.

Bug: 205169366
Test: atest AuthFsHostTest
Test: atest ComposHostTestCases
Test: composd_cmd forced-odrefresh
      # exit 80 without ART hack, with permissive SELinux
Change-Id: I2405baedae9ba2be5e84eb84d3228f7be080f8c6
diff --git a/authfs/Android.bp b/authfs/Android.bp
index a6792b0..353b597 100644
--- a/authfs/Android.bp
+++ b/authfs/Android.bp
@@ -20,6 +20,7 @@
         "libfuse_rust",
         "liblibc",
         "liblog_rust",
+        "libnix",
         "libstructopt",
         "libthiserror",
     ],
diff --git a/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl b/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl
index 1c618a1..43dee52 100644
--- a/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl
+++ b/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl
@@ -68,17 +68,19 @@
      * Creates a file given the remote directory FD.
      *
      * @param basename The file name to create. Must not contain directory separator.
+     * @param mode File mode of the new file. See open(2).
      * @return file A remote FD that represents the new created file.
      */
-    int createFileInDirectory(int dirFd, String basename);
+    int createFileInDirectory(int dirFd, String basename, int mode);
 
     /**
      * Creates a directory inside the given remote directory FD.
      *
      * @param basename The directory name to create. Must not contain directory separator.
+     * @param mode File mode of the new directory. See mkdir(2).
      * @return file FD that represents the new created directory.
      */
-    int createDirectoryInDirectory(int dirFd, String basename);
+    int createDirectoryInDirectory(int dirFd, String basename, int mode);
 
     /**
      * Deletes a file in the given directory.
@@ -94,6 +96,14 @@
      */
     void deleteDirectory(int dirFd, String basename);
 
+    /**
+     * Changes mode of the FD.
+     *
+     * @param fd The FD to change.
+     * @param mode New file mode to pass to chmod(2)/fchmod(2).
+     */
+    void chmod(int fd, int mode);
+
     /** Filesystem stats that AuthFS is interested in.*/
     parcelable FsStat {
         /** Block size of the filesystem */
diff --git a/authfs/fd_server/src/aidl.rs b/authfs/fd_server/src/aidl.rs
index 66c943e..c2206c8 100644
--- a/authfs/fd_server/src/aidl.rs
+++ b/authfs/fd_server/src/aidl.rs
@@ -17,8 +17,9 @@
 use anyhow::Result;
 use log::error;
 use nix::{
-    dir::Dir, errno::Errno, fcntl::openat, fcntl::OFlag, sys::stat::mkdirat, sys::stat::Mode,
-    sys::statvfs::statvfs, sys::statvfs::Statvfs, unistd::unlinkat, unistd::UnlinkatFlags,
+    dir::Dir, errno::Errno, fcntl::openat, fcntl::OFlag, sys::stat::fchmod, sys::stat::mkdirat,
+    sys::stat::mode_t, sys::stat::Mode, sys::statvfs::statvfs, sys::statvfs::Statvfs,
+    unistd::unlinkat, unistd::UnlinkatFlags,
 };
 use std::cmp::min;
 use std::collections::{btree_map, BTreeMap};
@@ -39,17 +40,8 @@
 };
 use binder_common::{new_binder_exception, new_binder_service_specific_error};
 
-fn validate_and_cast_offset(offset: i64) -> Result<u64, Status> {
-    offset.try_into().map_err(|_| new_errno_error(Errno::EINVAL))
-}
-
-fn validate_and_cast_size(size: i32) -> Result<usize, Status> {
-    if size > MAX_REQUESTING_DATA {
-        Err(new_errno_error(Errno::EFBIG))
-    } else {
-        size.try_into().map_err(|_| new_errno_error(Errno::EINVAL))
-    }
-}
+/// Bitflags of forbidden file mode, e.g. setuid, setgid and sticky bit.
+const FORBIDDEN_MODES: Mode = Mode::from_bits_truncate(!0o777);
 
 /// Configuration of a file descriptor to be served/exposed/shared.
 pub enum FdConfig {
@@ -288,19 +280,20 @@
         })
     }
 
-    fn createFileInDirectory(&self, dir_fd: i32, basename: &str) -> BinderResult<i32> {
+    fn createFileInDirectory(&self, dir_fd: i32, basename: &str, mode: i32) -> BinderResult<i32> {
         validate_basename(basename)?;
 
         self.insert_new_fd(dir_fd, |config| match config {
             FdConfig::InputDir(_) => Err(new_errno_error(Errno::EACCES)),
             FdConfig::OutputDir(dir) => {
+                let mode = validate_file_mode(mode)?;
                 let new_fd = openat(
                     dir.as_raw_fd(),
                     basename,
                     // TODO(205172873): handle the case when the file already exist, e.g. truncate
                     // or fail, and possibly allow the client to specify. For now, always truncate.
                     OFlag::O_CREAT | OFlag::O_RDWR | OFlag::O_TRUNC,
-                    Mode::S_IRUSR | Mode::S_IWUSR,
+                    mode,
                 )
                 .map_err(new_errno_error)?;
                 // SAFETY: new_fd is just created and not an error.
@@ -311,13 +304,19 @@
         })
     }
 
-    fn createDirectoryInDirectory(&self, dir_fd: i32, basename: &str) -> BinderResult<i32> {
+    fn createDirectoryInDirectory(
+        &self,
+        dir_fd: i32,
+        basename: &str,
+        mode: i32,
+    ) -> BinderResult<i32> {
         validate_basename(basename)?;
 
         self.insert_new_fd(dir_fd, |config| match config {
             FdConfig::InputDir(_) => Err(new_errno_error(Errno::EACCES)),
             FdConfig::OutputDir(_) => {
-                mkdirat(dir_fd, basename, Mode::S_IRWXU).map_err(new_errno_error)?;
+                let mode = validate_file_mode(mode)?;
+                mkdirat(dir_fd, basename, mode).map_err(new_errno_error)?;
                 let new_dir = Dir::openat(
                     dir_fd,
                     basename,
@@ -359,6 +358,16 @@
         })
     }
 
+    fn chmod(&self, fd: i32, mode: i32) -> BinderResult<()> {
+        self.handle_fd(fd, |config| match config {
+            FdConfig::ReadWrite(_) | FdConfig::OutputDir(_) => {
+                let mode = validate_file_mode(mode)?;
+                fchmod(fd, mode).map_err(new_errno_error)
+            }
+            _ => Err(new_errno_error(Errno::EACCES)),
+        })
+    }
+
     fn statfs(&self) -> BinderResult<FsStat> {
         let st = statvfs("/data").map_err(new_errno_error)?;
         try_into_fs_stat(st).map_err(|_e| new_errno_error(Errno::EINVAL))
@@ -395,6 +404,18 @@
     Ok(new_file)
 }
 
+fn validate_and_cast_offset(offset: i64) -> Result<u64, Status> {
+    offset.try_into().map_err(|_| new_errno_error(Errno::EINVAL))
+}
+
+fn validate_and_cast_size(size: i32) -> Result<usize, Status> {
+    if size > MAX_REQUESTING_DATA {
+        Err(new_errno_error(Errno::EFBIG))
+    } else {
+        size.try_into().map_err(|_| new_errno_error(Errno::EINVAL))
+    }
+}
+
 fn validate_basename(name: &str) -> BinderResult<()> {
     if name.contains(MAIN_SEPARATOR) {
         Err(new_errno_error(Errno::EINVAL))
@@ -402,3 +423,12 @@
         Ok(())
     }
 }
+
+fn validate_file_mode(mode: i32) -> BinderResult<Mode> {
+    let mode = Mode::from_bits(mode as mode_t).ok_or_else(|| new_errno_error(Errno::EINVAL))?;
+    if mode.intersects(FORBIDDEN_MODES) {
+        Err(new_errno_error(Errno::EPERM))
+    } else {
+        Ok(mode)
+    }
+}
diff --git a/authfs/src/file.rs b/authfs/src/file.rs
index 6353209..9bbf3ef 100644
--- a/authfs/src/file.rs
+++ b/authfs/src/file.rs
@@ -1,6 +1,8 @@
+mod attr;
 mod dir;
 mod remote_file;
 
+pub use attr::Attr;
 pub use dir::{InMemoryDir, RemoteDirEditor};
 pub use remote_file::{RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader};
 
diff --git a/authfs/src/file/attr.rs b/authfs/src/file/attr.rs
new file mode 100644
index 0000000..48084aa
--- /dev/null
+++ b/authfs/src/file/attr.rs
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use log::error;
+use nix::sys::stat::{mode_t, Mode, SFlag};
+use std::io;
+
+use super::VirtFdService;
+
+/// Default/assumed mode of files not created by authfs.
+///
+/// For files that are given to authfs as FDs (i.e. not created through authfs), their mode is
+/// unknown (or untrusted) until it is ever set. The default mode is just to make it
+/// readable/writable to VFS. When the mode is set, the value on fd_server is supposed to become
+/// consistent.
+const DEFAULT_FILE_MODE: Mode =
+    Mode::from_bits_truncate(Mode::S_IRUSR.bits() | Mode::S_IWUSR.bits());
+
+/// Default/assumed mode of directories not created by authfs.
+///
+/// See above.
+const DEFAULT_DIR_MODE: Mode = Mode::S_IRWXU;
+
+/// `Attr` maintains the local truth for attributes (e.g. mode and type) while allowing setting the
+/// remote attribute for the file description.
+pub struct Attr {
+    service: VirtFdService,
+    mode: Mode,
+    remote_fd: i32,
+    is_dir: bool,
+}
+
+impl Attr {
+    pub fn new_file(service: VirtFdService, remote_fd: i32) -> Attr {
+        Attr { service, mode: DEFAULT_FILE_MODE, remote_fd, is_dir: false }
+    }
+
+    pub fn new_dir(service: VirtFdService, remote_fd: i32) -> Attr {
+        Attr { service, mode: DEFAULT_DIR_MODE, remote_fd, is_dir: true }
+    }
+
+    pub fn new_file_with_mode(service: VirtFdService, remote_fd: i32, mode: mode_t) -> Attr {
+        Attr { service, mode: Mode::from_bits_truncate(mode), remote_fd, is_dir: false }
+    }
+
+    pub fn new_dir_with_mode(service: VirtFdService, remote_fd: i32, mode: mode_t) -> Attr {
+        Attr { service, mode: Mode::from_bits_truncate(mode), remote_fd, is_dir: true }
+    }
+
+    pub fn mode(&self) -> u32 {
+        self.mode.bits()
+    }
+
+    /// Sets the file mode.
+    ///
+    /// In addition to the actual file mode, `encoded_mode` also contains information of the file
+    /// type.
+    pub fn set_mode(&mut self, encoded_mode: u32) -> io::Result<()> {
+        let new_sflag = SFlag::from_bits_truncate(encoded_mode);
+        let new_mode = Mode::from_bits_truncate(encoded_mode);
+
+        let type_flag = if self.is_dir { SFlag::S_IFDIR } else { SFlag::S_IFREG };
+        if !type_flag.contains(new_sflag) {
+            return Err(io::Error::from_raw_os_error(libc::EINVAL));
+        }
+
+        // Request for update only if changing.
+        if new_mode != self.mode {
+            self.service.chmod(self.remote_fd, new_mode.bits() as i32).map_err(|e| {
+                error!(
+                    "Failed to chmod (fd: {}, mode: {:o}) on fd_server: {:?}",
+                    self.remote_fd, new_mode, e
+                );
+                io::Error::from_raw_os_error(libc::EIO)
+            })?;
+            self.mode = new_mode;
+        }
+        Ok(())
+    }
+}
diff --git a/authfs/src/file/dir.rs b/authfs/src/file/dir.rs
index 2a8f359..7f26fd7 100644
--- a/authfs/src/file/dir.rs
+++ b/authfs/src/file/dir.rs
@@ -15,10 +15,12 @@
  */
 
 use log::warn;
+use nix::sys::stat::Mode;
 use std::collections::{hash_map, HashMap};
 use std::io;
 use std::path::{Path, PathBuf};
 
+use super::attr::Attr;
 use super::remote_file::RemoteFileEditor;
 use super::{validate_basename, VirtFdService, VirtFdServiceStatus};
 use crate::fsverity::VerifiedFileEditor;
@@ -74,37 +76,43 @@
         &mut self,
         basename: &Path,
         inode: Inode,
-    ) -> io::Result<VerifiedFileEditor<RemoteFileEditor>> {
-        self.validate_argument(basename)?;
-
+        mode: libc::mode_t,
+    ) -> io::Result<(VerifiedFileEditor<RemoteFileEditor>, Attr)> {
+        let mode = self.validate_arguments(basename, mode)?;
         let basename_str =
             basename.to_str().ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?;
         let new_fd = self
             .service
-            .createFileInDirectory(self.remote_dir_fd, basename_str)
+            .createFileInDirectory(self.remote_dir_fd, basename_str, mode as i32)
             .map_err(into_io_error)?;
 
         let new_remote_file =
             VerifiedFileEditor::new(RemoteFileEditor::new(self.service.clone(), new_fd));
         self.entries.insert(basename.to_path_buf(), DirEntry { inode, is_dir: false });
-        Ok(new_remote_file)
+        let new_attr = Attr::new_file_with_mode(self.service.clone(), new_fd, mode);
+        Ok((new_remote_file, new_attr))
     }
 
     /// Creates a remote directory named `basename` with corresponding `inode` at the current
     /// directory.
-    pub fn mkdir(&mut self, basename: &Path, inode: Inode) -> io::Result<RemoteDirEditor> {
-        self.validate_argument(basename)?;
-
+    pub fn mkdir(
+        &mut self,
+        basename: &Path,
+        inode: Inode,
+        mode: libc::mode_t,
+    ) -> io::Result<(RemoteDirEditor, Attr)> {
+        let mode = self.validate_arguments(basename, mode)?;
         let basename_str =
             basename.to_str().ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?;
         let new_fd = self
             .service
-            .createDirectoryInDirectory(self.remote_dir_fd, basename_str)
+            .createDirectoryInDirectory(self.remote_dir_fd, basename_str, mode as i32)
             .map_err(into_io_error)?;
 
         let new_remote_dir = RemoteDirEditor::new(self.service.clone(), new_fd);
         self.entries.insert(basename.to_path_buf(), DirEntry { inode, is_dir: true });
-        Ok(new_remote_dir)
+        let new_attr = Attr::new_dir_with_mode(self.service.clone(), new_fd, mode);
+        Ok((new_remote_dir, new_attr))
     }
 
     /// Deletes a file
@@ -167,17 +175,19 @@
         }
     }
 
-    fn validate_argument(&self, basename: &Path) -> io::Result<()> {
+    fn validate_arguments(&self, basename: &Path, mode: u32) -> io::Result<u32> {
         // Kernel should only give us a basename.
         debug_assert!(validate_basename(basename).is_ok());
 
         if self.entries.contains_key(basename) {
-            Err(io::Error::from_raw_os_error(libc::EEXIST))
-        } else if self.entries.len() >= MAX_ENTRIES.into() {
-            Err(io::Error::from_raw_os_error(libc::EMLINK))
-        } else {
-            Ok(())
+            return Err(io::Error::from_raw_os_error(libc::EEXIST));
         }
+
+        if self.entries.len() >= MAX_ENTRIES.into() {
+            return Err(io::Error::from_raw_os_error(libc::EMLINK));
+        }
+
+        Ok(Mode::from_bits_truncate(mode).bits())
     }
 }
 
diff --git a/authfs/src/fusefs.rs b/authfs/src/fusefs.rs
index 17a368f..85a8371 100644
--- a/authfs/src/fusefs.rs
+++ b/authfs/src/fusefs.rs
@@ -37,8 +37,8 @@
 
 use crate::common::{divide_roundup, ChunkedSizeIter, CHUNK_SIZE};
 use crate::file::{
-    validate_basename, InMemoryDir, RandomWrite, ReadByChunk, RemoteDirEditor, RemoteFileEditor,
-    RemoteFileReader, RemoteMerkleTreeReader,
+    validate_basename, Attr, InMemoryDir, RandomWrite, ReadByChunk, RemoteDirEditor,
+    RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader,
 };
 use crate::fsstat::RemoteFsStatsReader;
 use crate::fsverity::{VerifiedFileEditor, VerifiedFileReader};
@@ -66,16 +66,16 @@
     UnverifiedReadonly { reader: RemoteFileReader, file_size: u64 },
     /// A file type that is initially empty, and the content is stored on a remote server. File
     /// integrity is guaranteed with private Merkle tree.
-    VerifiedNew { editor: VerifiedFileEditor<RemoteFileEditor> },
+    VerifiedNew { editor: VerifiedFileEditor<RemoteFileEditor>, attr: Attr },
     /// A directory type that is initially empty. One can create new file (`VerifiedNew`) and new
     /// directory (`VerifiedNewDirectory` itself) with integrity guaranteed within the VM.
-    VerifiedNewDirectory { dir: RemoteDirEditor },
+    VerifiedNewDirectory { dir: RemoteDirEditor, attr: Attr },
 }
 
 impl AuthFsEntry {
-    fn expect_empty_writable_directory(&self) -> io::Result<()> {
+    fn expect_empty_deletable_directory(&self) -> io::Result<()> {
         match self {
-            AuthFsEntry::VerifiedNewDirectory { dir } => {
+            AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
                 if dir.number_of_entries() == 0 {
                     Ok(())
                 } else {
@@ -304,7 +304,7 @@
 #[allow(clippy::enum_variant_names)]
 enum AccessMode {
     ReadOnly,
-    ReadWrite,
+    Variable(u32),
 }
 
 fn create_stat(
@@ -317,10 +317,11 @@
 
     st.st_ino = ino;
     st.st_mode = match access_mode {
-        // Until needed, let's just grant the owner access.
-        // TODO(205169366): Implement mode properly.
-        AccessMode::ReadOnly => libc::S_IFREG | libc::S_IRUSR,
-        AccessMode::ReadWrite => libc::S_IFREG | libc::S_IRUSR | libc::S_IWUSR,
+        AccessMode::ReadOnly => {
+            // Until needed, let's just grant the owner access.
+            libc::S_IFREG | libc::S_IRUSR
+        }
+        AccessMode::Variable(mode) => libc::S_IFREG | mode,
     };
     st.st_nlink = 1;
     st.st_uid = 0;
@@ -334,18 +335,22 @@
     Ok(st)
 }
 
-fn create_dir_stat(ino: libc::ino_t, file_number: u16) -> io::Result<libc::stat64> {
+fn create_dir_stat(
+    ino: libc::ino_t,
+    file_number: u16,
+    access_mode: AccessMode,
+) -> io::Result<libc::stat64> {
     // SAFETY: stat64 is a plan C struct without pointer.
     let mut st = unsafe { MaybeUninit::<libc::stat64>::zeroed().assume_init() };
 
     st.st_ino = ino;
-    // TODO(205169366): Implement mode properly.
-    st.st_mode = libc::S_IFDIR
-        | libc::S_IXUSR
-        | libc::S_IWUSR
-        | libc::S_IRUSR
-        | libc::S_IXGRP
-        | libc::S_IXOTH;
+    st.st_mode = match access_mode {
+        AccessMode::ReadOnly => {
+            // Until needed, let's just grant the owner access and search to group and others.
+            libc::S_IFDIR | libc::S_IXUSR | libc::S_IRUSR | libc::S_IXGRP | libc::S_IXOTH
+        }
+        AccessMode::Variable(mode) => libc::S_IFDIR | mode,
+    };
 
     // 2 extra for . and ..
     st.st_nlink = file_number
@@ -431,7 +436,7 @@
                     let path = cstr_to_path(name);
                     dir.lookup_inode(path).ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))
                 }
-                AuthFsEntry::VerifiedNewDirectory { dir } => {
+                AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
                     let path = cstr_to_path(name);
                     dir.find_inode(path)
                 }
@@ -445,18 +450,20 @@
             |InodeState { entry, handle_ref_count, .. }| {
                 let st = match entry {
                     AuthFsEntry::ReadonlyDirectory { dir } => {
-                        create_dir_stat(inode, dir.number_of_entries())
+                        create_dir_stat(inode, dir.number_of_entries(), AccessMode::ReadOnly)
                     }
                     AuthFsEntry::UnverifiedReadonly { file_size, .. }
                     | AuthFsEntry::VerifiedReadonly { file_size, .. } => {
                         create_stat(inode, *file_size, AccessMode::ReadOnly)
                     }
-                    AuthFsEntry::VerifiedNew { editor } => {
-                        create_stat(inode, editor.size(), AccessMode::ReadWrite)
+                    AuthFsEntry::VerifiedNew { editor, attr, .. } => {
+                        create_stat(inode, editor.size(), AccessMode::Variable(attr.mode()))
                     }
-                    AuthFsEntry::VerifiedNewDirectory { dir } => {
-                        create_dir_stat(inode, dir.number_of_entries())
-                    }
+                    AuthFsEntry::VerifiedNewDirectory { dir, attr } => create_dir_stat(
+                        inode,
+                        dir.number_of_entries(),
+                        AccessMode::Variable(attr.mode()),
+                    ),
                 }?;
                 *handle_ref_count += 1;
                 Ok(st)
@@ -514,18 +521,20 @@
             Ok((
                 match config {
                     AuthFsEntry::ReadonlyDirectory { dir } => {
-                        create_dir_stat(inode, dir.number_of_entries())
+                        create_dir_stat(inode, dir.number_of_entries(), AccessMode::ReadOnly)
                     }
                     AuthFsEntry::UnverifiedReadonly { file_size, .. }
                     | AuthFsEntry::VerifiedReadonly { file_size, .. } => {
                         create_stat(inode, *file_size, AccessMode::ReadOnly)
                     }
-                    AuthFsEntry::VerifiedNew { editor } => {
-                        create_stat(inode, editor.size(), AccessMode::ReadWrite)
+                    AuthFsEntry::VerifiedNew { editor, attr, .. } => {
+                        create_stat(inode, editor.size(), AccessMode::Variable(attr.mode()))
                     }
-                    AuthFsEntry::VerifiedNewDirectory { dir } => {
-                        create_dir_stat(inode, dir.number_of_entries())
-                    }
+                    AuthFsEntry::VerifiedNewDirectory { dir, attr } => create_dir_stat(
+                        inode,
+                        dir.number_of_entries(),
+                        AccessMode::Variable(attr.mode()),
+                    ),
                 }?,
                 DEFAULT_METADATA_TIMEOUT,
             ))
@@ -546,8 +555,8 @@
                     check_access_mode(flags, libc::O_RDONLY)?;
                 }
                 AuthFsEntry::VerifiedNew { .. } => {
-                    // No need to check access modes since all the modes are allowed to the
-                    // read-writable file.
+                    // TODO(victorhsieh): Imeplement ACL check using the attr and ctx. Always allow
+                    // for now.
                 }
                 AuthFsEntry::ReadonlyDirectory { .. }
                 | AuthFsEntry::VerifiedNewDirectory { .. } => {
@@ -566,22 +575,22 @@
         _ctx: Context,
         parent: Self::Inode,
         name: &CStr,
-        _mode: u32,
+        mode: u32,
         _flags: u32,
-        _umask: u32,
+        umask: u32,
     ) -> io::Result<(Entry, Option<Self::Handle>, fuse::sys::OpenOptions)> {
-        // TODO(205169366): Implement mode properly.
         // TODO(205172873): handle O_TRUNC and O_EXCL properly.
         let new_inode = self.create_new_entry_with_ref_count(
             parent,
             name,
             |parent_entry, basename, new_inode| match parent_entry {
-                AuthFsEntry::VerifiedNewDirectory { dir } => {
+                AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
                     if dir.has_entry(basename) {
                         return Err(io::Error::from_raw_os_error(libc::EEXIST));
                     }
-                    let new_file = dir.create_file(basename, new_inode)?;
-                    Ok(AuthFsEntry::VerifiedNew { editor: new_file })
+                    let mode = mode & !umask;
+                    let (new_file, new_attr) = dir.create_file(basename, new_inode, mode)?;
+                    Ok(AuthFsEntry::VerifiedNew { editor: new_file, attr: new_attr })
                 }
                 _ => Err(io::Error::from_raw_os_error(libc::EBADF)),
             },
@@ -591,7 +600,7 @@
             Entry {
                 inode: new_inode,
                 generation: 0,
-                attr: create_stat(new_inode, /* file_size */ 0, AccessMode::ReadWrite)?,
+                attr: create_stat(new_inode, /* file_size */ 0, AccessMode::Variable(mode))?,
                 entry_timeout: DEFAULT_METADATA_TIMEOUT,
                 attr_timeout: DEFAULT_METADATA_TIMEOUT,
             },
@@ -620,12 +629,15 @@
                 AuthFsEntry::UnverifiedReadonly { reader, file_size } => {
                     read_chunks(w, reader, *file_size, offset, size)
                 }
-                AuthFsEntry::VerifiedNew { editor } => {
+                AuthFsEntry::VerifiedNew { editor, .. } => {
                     // Note that with FsOptions::WRITEBACK_CACHE, it's possible for the kernel to
                     // request a read even if the file is open with O_WRONLY.
                     read_chunks(w, editor, editor.size(), offset, size)
                 }
-                _ => Err(io::Error::from_raw_os_error(libc::EBADF)),
+                AuthFsEntry::ReadonlyDirectory { .. }
+                | AuthFsEntry::VerifiedNewDirectory { .. } => {
+                    Err(io::Error::from_raw_os_error(libc::EISDIR))
+                }
             }
         })
     }
@@ -643,12 +655,17 @@
         _flags: u32,
     ) -> io::Result<usize> {
         self.handle_inode(&inode, |config| match config {
-            AuthFsEntry::VerifiedNew { editor } => {
+            AuthFsEntry::VerifiedNew { editor, .. } => {
                 let mut buf = vec![0; size as usize];
                 r.read_exact(&mut buf)?;
                 editor.write_at(&buf, offset)
             }
-            _ => Err(io::Error::from_raw_os_error(libc::EBADF)),
+            AuthFsEntry::VerifiedReadonly { .. } | AuthFsEntry::UnverifiedReadonly { .. } => {
+                Err(io::Error::from_raw_os_error(libc::EPERM))
+            }
+            AuthFsEntry::ReadonlyDirectory { .. } | AuthFsEntry::VerifiedNewDirectory { .. } => {
+                Err(io::Error::from_raw_os_error(libc::EISDIR))
+            }
         })
     }
 
@@ -656,55 +673,51 @@
         &self,
         _ctx: Context,
         inode: Inode,
-        attr: libc::stat64,
+        in_attr: libc::stat64,
         _handle: Option<Handle>,
         valid: SetattrValid,
     ) -> io::Result<(libc::stat64, Duration)> {
-        self.handle_inode(&inode, |config| {
-            match config {
-                AuthFsEntry::VerifiedNew { editor } => {
-                    // Initialize the default stat.
-                    let mut new_attr = create_stat(inode, editor.size(), AccessMode::ReadWrite)?;
-                    // `valid` indicates what fields in `attr` are valid. Update to return correctly.
-                    if valid.contains(SetattrValid::SIZE) {
-                        // st_size is i64, but the cast should be safe since kernel should not give a
-                        // negative size.
-                        debug_assert!(attr.st_size >= 0);
-                        new_attr.st_size = attr.st_size;
-                        editor.resize(attr.st_size as u64)?;
-                    }
+        let mut inode_table = self.inode_table.lock().unwrap();
+        handle_inode_mut_locked(&mut inode_table, &inode, |InodeState { entry, .. }| match entry {
+            AuthFsEntry::VerifiedNew { editor, attr } => {
+                check_unsupported_setattr_request(valid)?;
 
-                    if valid.contains(SetattrValid::MODE) {
-                        warn!("Changing st_mode is not currently supported");
-                        return Err(io::Error::from_raw_os_error(libc::ENOSYS));
-                    }
-                    if valid.contains(SetattrValid::UID) {
-                        warn!("Changing st_uid is not currently supported");
-                        return Err(io::Error::from_raw_os_error(libc::ENOSYS));
-                    }
-                    if valid.contains(SetattrValid::GID) {
-                        warn!("Changing st_gid is not currently supported");
-                        return Err(io::Error::from_raw_os_error(libc::ENOSYS));
-                    }
-                    if valid.contains(SetattrValid::CTIME) {
-                        debug!(
-                            "Ignoring ctime change as authfs does not maintain timestamp currently"
-                        );
-                    }
-                    if valid.intersects(SetattrValid::ATIME | SetattrValid::ATIME_NOW) {
-                        debug!(
-                            "Ignoring atime change as authfs does not maintain timestamp currently"
-                        );
-                    }
-                    if valid.intersects(SetattrValid::MTIME | SetattrValid::MTIME_NOW) {
-                        debug!(
-                            "Ignoring mtime change as authfs does not maintain timestamp currently"
-                        );
-                    }
-                    Ok((new_attr, DEFAULT_METADATA_TIMEOUT))
+                // Initialize the default stat.
+                let mut new_attr =
+                    create_stat(inode, editor.size(), AccessMode::Variable(attr.mode()))?;
+                // `valid` indicates what fields in `attr` are valid. Update to return correctly.
+                if valid.contains(SetattrValid::SIZE) {
+                    // st_size is i64, but the cast should be safe since kernel should not give a
+                    // negative size.
+                    debug_assert!(in_attr.st_size >= 0);
+                    new_attr.st_size = in_attr.st_size;
+                    editor.resize(in_attr.st_size as u64)?;
                 }
-                _ => Err(io::Error::from_raw_os_error(libc::EBADF)),
+                if valid.contains(SetattrValid::MODE) {
+                    attr.set_mode(in_attr.st_mode)?;
+                    new_attr.st_mode = in_attr.st_mode;
+                }
+                Ok((new_attr, DEFAULT_METADATA_TIMEOUT))
             }
+            AuthFsEntry::VerifiedNewDirectory { dir, attr } => {
+                check_unsupported_setattr_request(valid)?;
+                if valid.contains(SetattrValid::SIZE) {
+                    return Err(io::Error::from_raw_os_error(libc::EISDIR));
+                }
+
+                // Initialize the default stat.
+                let mut new_attr = create_dir_stat(
+                    inode,
+                    dir.number_of_entries(),
+                    AccessMode::Variable(attr.mode()),
+                )?;
+                if valid.contains(SetattrValid::MODE) {
+                    attr.set_mode(in_attr.st_mode)?;
+                    new_attr.st_mode = in_attr.st_mode;
+                }
+                Ok((new_attr, DEFAULT_METADATA_TIMEOUT))
+            }
+            _ => Err(io::Error::from_raw_os_error(libc::EPERM)),
         })
     }
 
@@ -717,7 +730,7 @@
     ) -> io::Result<GetxattrReply> {
         self.handle_inode(&inode, |config| {
             match config {
-                AuthFsEntry::VerifiedNew { editor } => {
+                AuthFsEntry::VerifiedNew { editor, .. } => {
                     // FUSE ioctl is limited, thus we can't implement fs-verity ioctls without a kernel
                     // change (see b/196635431). Until it's possible, use xattr to expose what we need
                     // as an authfs specific API.
@@ -747,20 +760,20 @@
         _ctx: Context,
         parent: Self::Inode,
         name: &CStr,
-        _mode: u32,
-        _umask: u32,
+        mode: u32,
+        umask: u32,
     ) -> io::Result<Entry> {
-        // TODO(205169366): Implement mode properly.
         let new_inode = self.create_new_entry_with_ref_count(
             parent,
             name,
             |parent_entry, basename, new_inode| match parent_entry {
-                AuthFsEntry::VerifiedNewDirectory { dir } => {
+                AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
                     if dir.has_entry(basename) {
                         return Err(io::Error::from_raw_os_error(libc::EEXIST));
                     }
-                    let new_dir = dir.mkdir(basename, new_inode)?;
-                    Ok(AuthFsEntry::VerifiedNewDirectory { dir: new_dir })
+                    let mode = mode & !umask;
+                    let (new_dir, new_attr) = dir.mkdir(basename, new_inode, mode)?;
+                    Ok(AuthFsEntry::VerifiedNewDirectory { dir: new_dir, attr: new_attr })
                 }
                 AuthFsEntry::ReadonlyDirectory { .. } => {
                     Err(io::Error::from_raw_os_error(libc::EACCES))
@@ -772,7 +785,7 @@
         Ok(Entry {
             inode: new_inode,
             generation: 0,
-            attr: create_dir_stat(new_inode, /* file_number */ 0)?,
+            attr: create_dir_stat(new_inode, /* file_number */ 0, AccessMode::Variable(mode))?,
             entry_timeout: DEFAULT_METADATA_TIMEOUT,
             attr_timeout: DEFAULT_METADATA_TIMEOUT,
         })
@@ -784,7 +797,7 @@
             &mut inode_table,
             &parent,
             |InodeState { entry, unlinked, .. }| match entry {
-                AuthFsEntry::VerifiedNewDirectory { dir } => {
+                AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
                     let basename: &Path = cstr_to_path(name);
                     // Delete the file from in both the local and remote directories.
                     let _inode = dir.delete_file(basename)?;
@@ -810,11 +823,11 @@
 
         // Check before actual removal, with readonly borrow.
         handle_inode_locked(&inode_table, &parent, |inode_state| match &inode_state.entry {
-            AuthFsEntry::VerifiedNewDirectory { dir } => {
+            AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
                 let basename: &Path = cstr_to_path(name);
                 let existing_inode = dir.find_inode(basename)?;
                 handle_inode_locked(&inode_table, &existing_inode, |inode_state| {
-                    inode_state.entry.expect_empty_writable_directory()
+                    inode_state.entry.expect_empty_deletable_directory()
                 })
             }
             AuthFsEntry::ReadonlyDirectory { .. } => {
@@ -829,7 +842,7 @@
             &mut inode_table,
             &parent,
             |InodeState { entry, unlinked, .. }| match entry {
-                AuthFsEntry::VerifiedNewDirectory { dir } => {
+                AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
                     let basename: &Path = cstr_to_path(name);
                     let _inode = dir.force_delete_directory(basename)?;
                     *unlinked = true;
@@ -897,6 +910,27 @@
     }
 }
 
+fn check_unsupported_setattr_request(valid: SetattrValid) -> io::Result<()> {
+    if valid.contains(SetattrValid::UID) {
+        warn!("Changing st_uid is not currently supported");
+        return Err(io::Error::from_raw_os_error(libc::ENOSYS));
+    }
+    if valid.contains(SetattrValid::GID) {
+        warn!("Changing st_gid is not currently supported");
+        return Err(io::Error::from_raw_os_error(libc::ENOSYS));
+    }
+    if valid.intersects(
+        SetattrValid::CTIME
+            | SetattrValid::ATIME
+            | SetattrValid::ATIME_NOW
+            | SetattrValid::MTIME
+            | SetattrValid::MTIME_NOW,
+    ) {
+        debug!("Ignoring ctime/atime/mtime change as authfs does not maintain timestamp currently");
+    }
+    Ok(())
+}
+
 fn cstr_to_path(cstr: &CStr) -> &Path {
     OsStr::from_bytes(cstr.to_bytes()).as_ref()
 }
diff --git a/authfs/src/main.rs b/authfs/src/main.rs
index a083381..421cc02 100644
--- a/authfs/src/main.rs
+++ b/authfs/src/main.rs
@@ -43,7 +43,7 @@
 
 use auth::FakeAuthenticator;
 use file::{
-    InMemoryDir, RemoteDirEditor, RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader,
+    Attr, InMemoryDir, RemoteDirEditor, RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader,
 };
 use fsstat::RemoteFsStatsReader;
 use fsverity::{VerifiedFileEditor, VerifiedFileReader};
@@ -194,16 +194,20 @@
     service: file::VirtFdService,
     remote_fd: i32,
 ) -> Result<AuthFsEntry> {
-    let remote_file = RemoteFileEditor::new(service, remote_fd);
-    Ok(AuthFsEntry::VerifiedNew { editor: VerifiedFileEditor::new(remote_file) })
+    let remote_file = RemoteFileEditor::new(service.clone(), remote_fd);
+    Ok(AuthFsEntry::VerifiedNew {
+        editor: VerifiedFileEditor::new(remote_file),
+        attr: Attr::new_file(service, remote_fd),
+    })
 }
 
 fn new_remote_new_verified_dir_entry(
     service: file::VirtFdService,
     remote_fd: i32,
 ) -> Result<AuthFsEntry> {
-    let dir = RemoteDirEditor::new(service, remote_fd);
-    Ok(AuthFsEntry::VerifiedNewDirectory { dir })
+    let dir = RemoteDirEditor::new(service.clone(), remote_fd);
+    let attr = Attr::new_dir(service, remote_fd);
+    Ok(AuthFsEntry::VerifiedNewDirectory { dir, attr })
 }
 
 fn prepare_root_dir_entries(
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
index e2188b9..494b082 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -519,6 +519,67 @@
     }
 
     @Test
+    public void testChmod_File() throws Exception {
+        // Setup
+        runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/file", "--rw-fds 3");
+        runAuthFsOnMicrodroid("--remote-new-rw-file 3 --cid " + VMADDR_CID_HOST);
+
+        // Action & Verify
+        // Change mode
+        runOnMicrodroid("chmod 321 " + MOUNT_DIR + "/3");
+        expectFileMode("--wx-w---x", MOUNT_DIR + "/3", TEST_OUTPUT_DIR + "/file");
+        // Can't set the disallowed bits
+        assertFailedOnMicrodroid("chmod +s " + MOUNT_DIR + "/3");
+        assertFailedOnMicrodroid("chmod +t " + MOUNT_DIR + "/3");
+    }
+
+    @Test
+    public void testChmod_Dir() throws Exception {
+        // Setup
+        runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3");
+        runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST);
+
+        // Action & Verify
+        String authfsOutputDir = MOUNT_DIR + "/3";
+        // Create with umask
+        runOnMicrodroid("umask 000; mkdir " + authfsOutputDir + "/dir");
+        runOnMicrodroid("umask 022; mkdir " + authfsOutputDir + "/dir/dir2");
+        expectFileMode("drwxrwxrwx", authfsOutputDir + "/dir", TEST_OUTPUT_DIR + "/dir");
+        expectFileMode("drwxr-xr-x", authfsOutputDir + "/dir/dir2", TEST_OUTPUT_DIR + "/dir/dir2");
+        // Change mode
+        runOnMicrodroid("chmod -w " + authfsOutputDir + "/dir/dir2");
+        expectFileMode("dr-xr-xr-x", authfsOutputDir + "/dir/dir2", TEST_OUTPUT_DIR + "/dir/dir2");
+        runOnMicrodroid("chmod 321 " + authfsOutputDir + "/dir");
+        expectFileMode("d-wx-w---x", authfsOutputDir + "/dir", TEST_OUTPUT_DIR + "/dir");
+        // Can't set the disallowed bits
+        assertFailedOnMicrodroid("chmod +s " + authfsOutputDir + "/dir/dir2");
+        assertFailedOnMicrodroid("chmod +t " + authfsOutputDir + "/dir");
+    }
+
+    @Test
+    public void testChmod_FileInOutputDirectory() throws Exception {
+        // Setup
+        runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3");
+        runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST);
+
+        // Action & Verify
+        String authfsOutputDir = MOUNT_DIR + "/3";
+        // Create with umask
+        runOnMicrodroid("umask 000; echo -n foo > " + authfsOutputDir + "/file");
+        runOnMicrodroid("umask 022; echo -n foo > " + authfsOutputDir + "/file2");
+        expectFileMode("-rw-rw-rw-", authfsOutputDir + "/file", TEST_OUTPUT_DIR + "/file");
+        expectFileMode("-rw-r--r--", authfsOutputDir + "/file2", TEST_OUTPUT_DIR + "/file2");
+        // Change mode
+        runOnMicrodroid("chmod -w " + authfsOutputDir + "/file");
+        expectFileMode("-r--r--r--", authfsOutputDir + "/file", TEST_OUTPUT_DIR + "/file");
+        runOnMicrodroid("chmod 321 " + authfsOutputDir + "/file2");
+        expectFileMode("--wx-w---x", authfsOutputDir + "/file2", TEST_OUTPUT_DIR + "/file2");
+        // Can't set the disallowed bits
+        assertFailedOnMicrodroid("chmod +s " + authfsOutputDir + "/file");
+        assertFailedOnMicrodroid("chmod +t " + authfsOutputDir + "/file2");
+    }
+
+    @Test
     public void testStatfs() throws Exception {
         // Setup
         runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3");
@@ -571,6 +632,15 @@
         }
     }
 
+    private void expectFileMode(String expected, String microdroidPath, String androidPath)
+            throws DeviceNotAvailableException {
+        String actual = runOnMicrodroid("stat -c '%A' " + microdroidPath);
+        assertEquals("Inconsistent mode for " + microdroidPath, expected, actual);
+
+        actual = sAndroid.run("stat -c '%A' " + androidPath);
+        assertEquals("Inconsistent mode for " + androidPath + " (android)", expected, actual);
+    }
+
     private void resizeFileOnMicrodroid(String path, long size) {
         runOnMicrodroid("truncate -c -s " + size + " " + path);
     }