| /* |
| * Copyright (c) 2013-2014 Motorola Mobility LLC |
| * Copyright (C) 2017 Google, Inc. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 and |
| * only version 2 as published by the Free Software Foundation. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| */ |
| |
| #include <linux/proc_fs.h> |
| #include <linux/hashtable.h> |
| #include <linux/syscalls.h> |
| #include <linux/fcntl.h> |
| #include <linux/ctype.h> |
| #include <linux/vmalloc.h> |
| #include <linux/security.h> |
| #include <linux/uaccess.h> |
| #include "esdfs.h" |
| |
| static struct qstr names_secure[] = { |
| QSTR_LITERAL("autorun.inf"), |
| QSTR_LITERAL(".android_secure"), |
| QSTR_LITERAL("android_secure"), |
| QSTR_LITERAL("") |
| }; |
| |
| /* special path name searches */ |
| static inline bool match_name(struct qstr *name, struct qstr names[]) |
| { |
| int i = 0; |
| |
| BUG_ON(!name); |
| for (i = 0; *names[i].name; i++) |
| if (qstr_case_eq(name, &names[i])) |
| return true; |
| |
| return false; |
| } |
| |
| unsigned esdfs_package_list_version; |
| |
| static void fixup_perms_by_flag(int flags, const struct qstr *key, |
| uint32_t userid) |
| { |
| esdfs_package_list_version++; |
| } |
| |
| static struct pkg_list esdfs_pkg_list = { |
| .update = fixup_perms_by_flag, |
| }; |
| |
| int esdfs_init_package_list(void) |
| { |
| pkglist_register_update_listener(&esdfs_pkg_list); |
| return 0; |
| } |
| |
| void esdfs_destroy_package_list(void) |
| { |
| pkglist_unregister_update_listener(&esdfs_pkg_list); |
| } |
| |
| /* |
| * Derive an entry's premissions tree position based on its parent. |
| */ |
| void esdfs_derive_perms(struct dentry *dentry) |
| { |
| struct esdfs_inode_info *inode_i = ESDFS_I(dentry->d_inode); |
| bool is_root; |
| int ret; |
| kuid_t appid; |
| struct qstr q_Download = QSTR_LITERAL("Download"); |
| struct qstr q_Android = QSTR_LITERAL("Android"); |
| struct qstr q_data = QSTR_LITERAL("data"); |
| struct qstr q_obb = QSTR_LITERAL("obb"); |
| struct qstr q_media = QSTR_LITERAL("media"); |
| struct qstr q_cache = QSTR_LITERAL("cache"); |
| struct qstr q_user = QSTR_LITERAL("user"); |
| struct esdfs_inode_info *parent_i = ESDFS_I(dentry->d_parent->d_inode); |
| |
| spin_lock(&dentry->d_lock); |
| is_root = IS_ROOT(dentry); |
| spin_unlock(&dentry->d_lock); |
| if (is_root) |
| return; |
| |
| /* Inherit from the parent to start */ |
| inode_i->tree = parent_i->tree; |
| inode_i->userid = parent_i->userid; |
| inode_i->appid = parent_i->appid; |
| inode_i->under_obb = parent_i->under_obb; |
| |
| /* |
| * ESDFS_TREE_MEDIA* are intentionally dead ends. |
| */ |
| switch (inode_i->tree) { |
| case ESDFS_TREE_ROOT_LEGACY: |
| inode_i->tree = ESDFS_TREE_ROOT; |
| ret = kstrtou32(dentry->d_name.name, 0, &inode_i->userid); |
| if (qstr_case_eq(&dentry->d_name, &q_obb)) |
| inode_i->tree = ESDFS_TREE_ANDROID_OBB; |
| break; |
| |
| case ESDFS_TREE_ROOT: |
| inode_i->tree = ESDFS_TREE_MEDIA; |
| if (qstr_case_eq(&dentry->d_name, &q_Download)) |
| inode_i->tree = ESDFS_TREE_DOWNLOAD; |
| else if (qstr_case_eq(&dentry->d_name, &q_Android)) |
| inode_i->tree = ESDFS_TREE_ANDROID; |
| break; |
| |
| case ESDFS_TREE_ANDROID: |
| if (qstr_case_eq(&dentry->d_name, &q_data)) { |
| inode_i->tree = ESDFS_TREE_ANDROID_DATA; |
| } else if (qstr_case_eq(&dentry->d_name, &q_obb)) { |
| inode_i->tree = ESDFS_TREE_ANDROID_OBB; |
| inode_i->under_obb = true; |
| } else if (qstr_case_eq(&dentry->d_name, &q_media)) { |
| inode_i->tree = ESDFS_TREE_ANDROID_MEDIA; |
| } else if (ESDFS_RESTRICT_PERMS(ESDFS_SB(dentry->d_sb)) && |
| qstr_case_eq(&dentry->d_name, &q_user)) { |
| inode_i->tree = ESDFS_TREE_ANDROID_USER; |
| } |
| break; |
| |
| case ESDFS_TREE_ANDROID_DATA: |
| case ESDFS_TREE_ANDROID_OBB: |
| case ESDFS_TREE_ANDROID_MEDIA: |
| appid = pkglist_get_allowed_appid(dentry->d_name.name, |
| inode_i->userid); |
| if (uid_valid(appid)) |
| inode_i->appid = esdfs_from_kuid( |
| ESDFS_SB(dentry->d_sb), appid); |
| else |
| inode_i->appid = 0; |
| inode_i->tree = ESDFS_TREE_ANDROID_APP; |
| break; |
| case ESDFS_TREE_ANDROID_APP: |
| if (qstr_case_eq(&dentry->d_name, &q_cache)) |
| inode_i->tree = ESDFS_TREE_ANDROID_APP_CACHE; |
| break; |
| case ESDFS_TREE_ANDROID_USER: |
| /* Another user, so start over */ |
| inode_i->tree = ESDFS_TREE_ROOT; |
| ret = kstrtou32(dentry->d_name.name, 0, &inode_i->userid); |
| break; |
| } |
| } |
| |
| /* Apply tree position-specific permissions */ |
| void esdfs_set_derived_perms(struct inode *inode) |
| { |
| struct esdfs_sb_info *sbi = ESDFS_SB(inode->i_sb); |
| struct esdfs_inode_info *inode_i = ESDFS_I(inode); |
| gid_t gid = sbi->upper_perms.gid; |
| |
| esdfs_i_uid_write(inode, sbi->upper_perms.uid); |
| inode->i_mode &= S_IFMT; |
| if (ESDFS_RESTRICT_PERMS(sbi)) |
| esdfs_i_gid_write(inode, gid); |
| else { |
| if (gid == AID_SDCARD_RW && !test_opt(sbi, DEFAULT_NORMAL)) |
| esdfs_i_gid_write(inode, AID_SDCARD_RW); |
| else |
| esdfs_i_gid_write(inode, derive_uid(inode_i, gid)); |
| inode->i_mode |= sbi->upper_perms.dmask; |
| } |
| |
| switch (inode_i->tree) { |
| case ESDFS_TREE_ROOT_LEGACY: |
| if (ESDFS_RESTRICT_PERMS(sbi)) |
| inode->i_mode |= sbi->upper_perms.dmask; |
| else if (test_opt(sbi, DERIVE_MULTI)) { |
| inode->i_mode &= S_IFMT; |
| inode->i_mode |= 0711; |
| } |
| break; |
| |
| case ESDFS_TREE_NONE: |
| case ESDFS_TREE_ROOT: |
| if (ESDFS_RESTRICT_PERMS(sbi)) { |
| esdfs_i_gid_write(inode, AID_SDCARD_R); |
| inode->i_mode |= sbi->upper_perms.dmask; |
| } else if (test_opt(sbi, DERIVE_PUBLIC) && |
| test_opt(ESDFS_SB(inode->i_sb), DERIVE_CONFINE)) { |
| inode->i_mode &= S_IFMT; |
| inode->i_mode |= 0771; |
| } |
| break; |
| |
| case ESDFS_TREE_MEDIA: |
| if (ESDFS_RESTRICT_PERMS(sbi)) { |
| esdfs_i_gid_write(inode, AID_SDCARD_R); |
| inode->i_mode |= 0770; |
| } |
| break; |
| |
| case ESDFS_TREE_DOWNLOAD: |
| case ESDFS_TREE_ANDROID: |
| case ESDFS_TREE_ANDROID_DATA: |
| case ESDFS_TREE_ANDROID_OBB: |
| case ESDFS_TREE_ANDROID_MEDIA: |
| if (ESDFS_RESTRICT_PERMS(sbi)) |
| inode->i_mode |= 0771; |
| break; |
| |
| case ESDFS_TREE_ANDROID_APP: |
| case ESDFS_TREE_ANDROID_APP_CACHE: |
| if (inode_i->appid) |
| esdfs_i_uid_write(inode, derive_uid(inode_i, |
| inode_i->appid)); |
| if (ESDFS_RESTRICT_PERMS(sbi)) |
| inode->i_mode |= 0770; |
| break; |
| |
| case ESDFS_TREE_ANDROID_USER: |
| if (ESDFS_RESTRICT_PERMS(sbi)) { |
| esdfs_i_gid_write(inode, AID_SDCARD_ALL); |
| inode->i_mode |= 0770; |
| } |
| inode->i_mode |= 0770; |
| break; |
| } |
| |
| /* strip execute bits from any non-directories */ |
| if (!S_ISDIR(inode->i_mode)) |
| inode->i_mode &= ~S_IXUGO; |
| } |
| |
| /* |
| * Before rerouting a lookup to follow a pseudo hard link, make sure that |
| * a stub exists at the source. Without it, readdir won't see an entry there |
| * resulting in a strange user experience. |
| */ |
| static int lookup_link_source(struct dentry *dentry, struct dentry *parent) |
| { |
| struct path lower_parent_path, lower_path; |
| int err; |
| |
| esdfs_get_lower_path(parent, &lower_parent_path); |
| |
| /* Check if the stub user profile folder is there. */ |
| err = esdfs_lookup_nocase(&lower_parent_path, &dentry->d_name, |
| &lower_path); |
| /* Remember it to handle renames and removal. */ |
| if (!err) |
| esdfs_set_lower_stub_path(dentry, &lower_path); |
| |
| esdfs_put_lower_path(parent, &lower_parent_path); |
| |
| return err; |
| } |
| |
| int esdfs_is_dl_lookup(struct dentry *dentry, struct dentry *parent) |
| { |
| struct esdfs_sb_info *sbi = ESDFS_SB(parent->d_sb); |
| struct esdfs_inode_info *parent_i = ESDFS_I(parent->d_inode); |
| /* |
| * Return 1 if this is the Download directory: |
| * The test for download checks: |
| * 1. The parent is the mount root. |
| * 2. The directory is named 'Download'. |
| * 3. The stub for the directory exists. |
| */ |
| if (test_opt(sbi, SPECIAL_DOWNLOAD) && |
| parent_i->tree == ESDFS_TREE_ROOT && |
| ESDFS_DENTRY_NEEDS_DL_LINK(dentry) && |
| lookup_link_source(dentry, parent) == 0) { |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| int esdfs_derived_lookup(struct dentry *dentry, struct dentry **parent) |
| { |
| struct esdfs_sb_info *sbi = ESDFS_SB((*parent)->d_sb); |
| struct esdfs_inode_info *parent_i = ESDFS_I((*parent)->d_inode); |
| struct qstr q_Android = QSTR_LITERAL("Android"); |
| |
| /* Deny access to security-sensitive entries. */ |
| if (ESDFS_I((*parent)->d_inode)->tree == ESDFS_TREE_ROOT && |
| match_name(&dentry->d_name, names_secure)) { |
| pr_debug("esdfs: denying access to: %s", dentry->d_name.name); |
| return -EACCES; |
| } |
| |
| /* Pin the unified mode obb link parent as it flies by. */ |
| if (!sbi->obb_parent && |
| test_opt(sbi, DERIVE_UNIFIED) && |
| parent_i->tree == ESDFS_TREE_ROOT && |
| parent_i->userid == 0 && |
| qstr_case_eq(&dentry->d_name, &q_Android)) |
| sbi->obb_parent = dget(dentry); /* keep it pinned */ |
| |
| /* |
| * Handle obb directory "grafting" as a pseudo hard link by overriding |
| * its parent to point to the target obb directory's parent. The rest |
| * of the lookup process will take care of setting up the bottom half |
| * to point to the real obb directory. |
| */ |
| if (parent_i->tree == ESDFS_TREE_ANDROID && |
| ESDFS_DENTRY_NEEDS_LINK(dentry) && |
| lookup_link_source(dentry, *parent) == 0) { |
| BUG_ON(!sbi->obb_parent); |
| if (ESDFS_INODE_CAN_LINK((*parent)->d_inode)) |
| *parent = dget(sbi->obb_parent); |
| } |
| |
| return 0; |
| } |
| |
| int esdfs_derived_revalidate(struct dentry *dentry, struct dentry *parent) |
| { |
| /* |
| * If obb is not linked yet, it means the dentry is pointing to the |
| * stub. Invalidate the dentry to force another lookup. |
| */ |
| if (ESDFS_I(parent->d_inode)->tree == ESDFS_TREE_ANDROID && |
| ESDFS_INODE_CAN_LINK(dentry->d_inode) && |
| ESDFS_DENTRY_NEEDS_LINK(dentry) && |
| !ESDFS_DENTRY_IS_LINKED(dentry)) |
| return -ESTALE; |
| if (ESDFS_I(parent->d_inode)->tree == ESDFS_TREE_ROOT && |
| ESDFS_DENTRY_NEEDS_DL_LINK(dentry) && |
| !ESDFS_DENTRY_IS_LINKED(dentry)) |
| return -ESTALE; |
| return 0; |
| } |
| |
| /* |
| * Implement the extra checking that is done based on the caller's package |
| * list-based access rights. |
| */ |
| int esdfs_check_derived_permission(struct inode *inode, int mask) |
| { |
| const struct cred *cred; |
| uid_t uid, appid; |
| |
| /* |
| * If we don't need to restrict access based on app GIDs and confine |
| * writes to outside of the Android/... tree, we can skip all of this. |
| */ |
| if (!ESDFS_RESTRICT_PERMS(ESDFS_SB(inode->i_sb)) && |
| !test_opt(ESDFS_SB(inode->i_sb), DERIVE_CONFINE)) |
| return 0; |
| |
| cred = current_cred(); |
| uid = from_kuid(&init_user_ns, cred->uid); |
| appid = uid % PKG_APPID_PER_USER; |
| |
| /* Reads, owners, and root are always granted access */ |
| if (!(mask & (MAY_WRITE | ESDFS_MAY_CREATE)) || |
| uid == 0 || uid_eq(cred->uid, inode->i_uid)) |
| return 0; |
| |
| /* |
| * Grant access to sdcard_rw holders, unless we are in unified mode |
| * and we are trying to write to the protected /Android tree or to |
| * create files in the root (aka, "confined" access). |
| */ |
| if ((!test_opt(ESDFS_SB(inode->i_sb), DERIVE_UNIFIED) || |
| (ESDFS_I(inode)->tree != ESDFS_TREE_ANDROID && |
| ESDFS_I(inode)->tree != ESDFS_TREE_DOWNLOAD && |
| ESDFS_I(inode)->tree != ESDFS_TREE_ANDROID_DATA && |
| ESDFS_I(inode)->tree != ESDFS_TREE_ANDROID_OBB && |
| ESDFS_I(inode)->tree != ESDFS_TREE_ANDROID_MEDIA && |
| ESDFS_I(inode)->tree != ESDFS_TREE_ANDROID_APP && |
| ESDFS_I(inode)->tree != ESDFS_TREE_ANDROID_APP_CACHE && |
| (ESDFS_I(inode)->tree != ESDFS_TREE_ROOT || |
| !(mask & ESDFS_MAY_CREATE))))) |
| return 0; |
| |
| pr_debug("esdfs: %s: denying access to appid: %u\n", __func__, appid); |
| return -EACCES; |
| } |
| |
| static gid_t get_type(struct esdfs_sb_info *sbi, const char *name) |
| { |
| const char *ext = strrchr(name, '.'); |
| kgid_t id; |
| |
| if (ext && ext[0]) { |
| ext = &ext[1]; |
| id = pkglist_get_ext_gid(ext); |
| return gid_valid(id)?esdfs_from_kgid(sbi, id):AID_MEDIA_RW; |
| } |
| return AID_MEDIA_RW; |
| } |
| |
| static kuid_t esdfs_get_derived_lower_uid(struct esdfs_sb_info *sbi, |
| struct esdfs_inode_info *info) |
| { |
| uid_t uid = sbi->lower_perms.uid; |
| int perm; |
| |
| perm = info->tree; |
| if (info->under_obb) |
| perm = ESDFS_TREE_ANDROID_OBB; |
| |
| switch (perm) { |
| case ESDFS_TREE_DOWNLOAD: |
| if (test_opt(sbi, SPECIAL_DOWNLOAD)) |
| return make_kuid(sbi->dl_ns, |
| sbi->lower_dl_perms.raw_uid); |
| /* fall through */ |
| case ESDFS_TREE_ROOT: |
| case ESDFS_TREE_MEDIA: |
| case ESDFS_TREE_ANDROID: |
| case ESDFS_TREE_ANDROID_DATA: |
| case ESDFS_TREE_ANDROID_MEDIA: |
| case ESDFS_TREE_ANDROID_APP: |
| case ESDFS_TREE_ANDROID_APP_CACHE: |
| uid = derive_uid(info, uid); |
| break; |
| case ESDFS_TREE_ANDROID_OBB: |
| uid = AID_MEDIA_OBB; |
| break; |
| case ESDFS_TREE_ROOT_LEGACY: |
| default: |
| break; |
| } |
| return esdfs_make_kuid(sbi, uid); |
| } |
| |
| static kgid_t esdfs_get_derived_lower_gid(struct esdfs_sb_info *sbi, |
| struct esdfs_inode_info *info, const char *name) |
| { |
| gid_t gid = sbi->lower_perms.gid; |
| uid_t upper_uid; |
| int perm; |
| |
| upper_uid = esdfs_i_uid_read(&info->vfs_inode); |
| perm = info->tree; |
| if (info->under_obb) |
| perm = ESDFS_TREE_ANDROID_OBB; |
| |
| switch (perm) { |
| case ESDFS_TREE_DOWNLOAD: |
| if (test_opt(sbi, SPECIAL_DOWNLOAD)) |
| return make_kgid(sbi->dl_ns, |
| sbi->lower_dl_perms.raw_gid); |
| /* fall through */ |
| case ESDFS_TREE_ROOT: |
| case ESDFS_TREE_MEDIA: |
| case ESDFS_TREE_ANDROID: |
| case ESDFS_TREE_ANDROID_DATA: |
| case ESDFS_TREE_ANDROID_MEDIA: |
| if (S_ISDIR(info->vfs_inode.i_mode)) |
| gid = derive_uid(info, AID_MEDIA_RW); |
| else |
| gid = derive_uid(info, get_type(sbi, name)); |
| break; |
| case ESDFS_TREE_ANDROID_OBB: |
| gid = AID_MEDIA_OBB; |
| break; |
| case ESDFS_TREE_ANDROID_APP: |
| if (uid_is_app(upper_uid)) |
| gid = multiuser_get_ext_gid(upper_uid); |
| else |
| gid = derive_uid(info, AID_MEDIA_RW); |
| break; |
| case ESDFS_TREE_ANDROID_APP_CACHE: |
| if (uid_is_app(upper_uid)) |
| gid = multiuser_get_ext_cache_gid(upper_uid); |
| else |
| gid = derive_uid(info, AID_MEDIA_RW); |
| break; |
| case ESDFS_TREE_ROOT_LEGACY: |
| default: |
| break; |
| } |
| return esdfs_make_kgid(sbi, gid); |
| } |
| |
| void esdfs_derive_lower_ownership(struct dentry *dentry, const char *name) |
| { |
| struct path path; |
| struct inode *inode; |
| struct inode *delegated_inode = NULL; |
| int error; |
| struct esdfs_sb_info *sbi = ESDFS_SB(dentry->d_sb); |
| struct esdfs_inode_info *info = ESDFS_I(dentry->d_inode); |
| kuid_t kuid; |
| kgid_t kgid; |
| struct iattr newattrs; |
| |
| if (!test_opt(sbi, GID_DERIVATION)) |
| return; |
| |
| esdfs_get_lower_path(dentry, &path); |
| inode = path.dentry->d_inode; |
| kuid = esdfs_get_derived_lower_uid(sbi, info); |
| kgid = esdfs_get_derived_lower_gid(sbi, info, name); |
| if (!gid_eq(path.dentry->d_inode->i_gid, kgid) |
| || !uid_eq(path.dentry->d_inode->i_uid, kuid)) { |
| retry_deleg: |
| newattrs.ia_valid = ATTR_GID | ATTR_UID | ATTR_FORCE; |
| newattrs.ia_uid = kuid; |
| newattrs.ia_gid = kgid; |
| if (!S_ISDIR(inode->i_mode)) |
| newattrs.ia_valid |= ATTR_KILL_SUID | ATTR_KILL_SGID |
| | ATTR_KILL_PRIV; |
| inode_lock(inode); |
| error = security_path_chown(&path, newattrs.ia_uid, |
| newattrs.ia_gid); |
| if (!error) |
| error = notify_change(path.dentry, &newattrs, |
| &delegated_inode); |
| inode_unlock(inode); |
| if (delegated_inode) { |
| error = break_deleg_wait(&delegated_inode); |
| if (!error) |
| goto retry_deleg; |
| } |
| if (error) |
| pr_debug("esdfs: Failed to touch up lower fs gid/uid for %s\n", name); |
| } |
| esdfs_put_lower_path(dentry, &path); |
| } |
| |
| /* |
| * The sdcard service has a hack that creates .nomedia files along certain |
| * paths to stop MediaScanner. Create those here. |
| */ |
| int esdfs_derive_mkdir_contents(struct dentry *dir_dentry) |
| { |
| struct esdfs_inode_info *inode_i; |
| struct qstr nomedia; |
| struct dentry *lower_dentry; |
| struct path lower_dir_path, lower_path; |
| struct dentry *lower_parent_dentry = NULL; |
| umode_t mode; |
| int err = 0; |
| const struct cred *creds; |
| int mask = 0; |
| |
| if (!dir_dentry->d_inode) |
| return 0; |
| |
| inode_i = ESDFS_I(dir_dentry->d_inode); |
| |
| /* |
| * Only create .nomedia in Android/data and Android/obb, but never in |
| * pseudo link stubs. |
| */ |
| if ((inode_i->tree != ESDFS_TREE_ANDROID_DATA && |
| inode_i->tree != ESDFS_TREE_ANDROID_OBB) || |
| (ESDFS_INODE_CAN_LINK(dir_dentry->d_inode) && |
| ESDFS_DENTRY_NEEDS_LINK(dir_dentry) && |
| !ESDFS_DENTRY_IS_LINKED(dir_dentry))) |
| return 0; |
| |
| esdfs_get_lower_path(dir_dentry, &lower_dir_path); |
| |
| nomedia.name = ".nomedia"; |
| nomedia.len = strlen(nomedia.name); |
| nomedia.hash = full_name_hash(lower_dir_path.dentry, nomedia.name, |
| nomedia.len); |
| |
| /* check if lower has its own hash */ |
| if (lower_dir_path.dentry->d_flags & DCACHE_OP_HASH) |
| lower_dir_path.dentry->d_op->d_hash(lower_dir_path.dentry, |
| &nomedia); |
| |
| creds = esdfs_override_creds(ESDFS_SB(dir_dentry->d_sb), |
| inode_i, &mask); |
| /* See if the lower file is there already. */ |
| err = vfs_path_lookup(lower_dir_path.dentry, lower_dir_path.mnt, |
| nomedia.name, 0, &lower_path); |
| if (!err) |
| path_put(&lower_path); |
| /* If it's there or there was an error, we're done */ |
| if (!err || err != -ENOENT) |
| goto out; |
| |
| /* The lower file is not there. See if the dentry is in the cache. */ |
| lower_dentry = d_lookup(lower_dir_path.dentry, &nomedia); |
| if (!lower_dentry) { |
| /* It's not there, so create a negative lower dentry. */ |
| lower_dentry = d_alloc(lower_dir_path.dentry, &nomedia); |
| if (!lower_dentry) { |
| err = -ENOMEM; |
| goto out; |
| } |
| d_add(lower_dentry, NULL); |
| } |
| |
| /* Now create the lower file. */ |
| mode = S_IFREG; |
| lower_parent_dentry = lock_parent(lower_dentry); |
| esdfs_set_lower_mode(ESDFS_SB(dir_dentry->d_sb), inode_i, &mode); |
| err = vfs_create(lower_dir_path.dentry->d_inode, lower_dentry, mode, |
| true); |
| unlock_dir(lower_parent_dentry); |
| dput(lower_dentry); |
| |
| out: |
| esdfs_put_lower_path(dir_dentry, &lower_dir_path); |
| esdfs_revert_creds(creds, &mask); |
| return err; |
| } |