| // SPDX-License-Identifier: GPL-2.0-or-later | |
| /* | |
| * NTFS named stream handling. | |
| * | |
| * Copyright (c) 2026 LG Electronics Co., Ltd. | |
| */ | |
| #include <linux/file.h> | |
| #include <linux/namei.h> | |
| #include <linux/overflow.h> | |
| #include <uapi/linux/ntfs.h> | |
| #include "attrib.h" | |
| #include "dir.h" | |
| #include "iomap.h" | |
| #include "mft.h" | |
| #include "ntfs.h" | |
| #include "stream.h" | |
| #include "time.h" | |
| #define NTFS_STREAM_MAX_IO (16 * 1024 * 1024) | |
| /* | |
| * ntfs_check_stream_name() - Validate a named-stream name | |
| * @name: UTF-16LE stream name | |
| * @name_len: Length of @name in UTF-16 code units | |
| * | |
| * Reject empty, overlong, separator-containing, or malformed UTF-16 names. | |
| * | |
| * Return: 0 if valid, or -EINVAL otherwise. | |
| */ | |
| int ntfs_check_stream_name(const __le16 *name, unsigned int name_len) | |
| { | |
| unsigned int i; | |
| if (!name_len || name_len > NTFS_MAX_NAME_LEN) | |
| return -EINVAL; | |
| for (i = 0; i < name_len; i++) { | |
| u16 c = le16_to_cpu(name[i]); | |
| if (c == 0 || c == '/' || c == '\\' || c == ':') | |
| return -EINVAL; | |
| if (c >= 0xd800 && c <= 0xdbff) { | |
| if (++i >= name_len) | |
| return -EINVAL; | |
| c = le16_to_cpu(name[i]); | |
| if (c < 0xdc00 || c > 0xdfff) | |
| return -EINVAL; | |
| } else if (c >= 0xdc00 && c <= 0xdfff) { | |
| return -EINVAL; | |
| } | |
| } | |
| return 0; | |
| } | |
| /* | |
| * ntfs_stream_inode_refresh() - Refresh base-derived stream inode metadata | |
| * @vi: inode representing a named stream | |
| * | |
| * Synchronize ownership, mode, flags, timestamps, and generation with the | |
| * base inode. Stream size and allocation remain specific to the stream. | |
| */ | |
| void ntfs_stream_inode_refresh(struct inode *vi) | |
| { | |
| struct ntfs_inode *ni = NTFS_I(vi); | |
| struct inode *base_vi; | |
| if (!ntfs_inode_is_named_stream(ni) || ni->nr_extents != -1) | |
| return; | |
| base_vi = VFS_I(ni->ext.base_ntfs_ino); | |
| vi->i_uid = base_vi->i_uid; | |
| vi->i_gid = base_vi->i_gid; | |
| vi->i_mode = (base_vi->i_mode & ~S_IFMT) | S_IFREG; | |
| vi->i_flags = base_vi->i_flags; | |
| inode_set_mtime_to_ts(vi, inode_get_mtime(base_vi)); | |
| inode_set_ctime_to_ts(vi, inode_get_ctime(base_vi)); | |
| inode_set_atime_to_ts(vi, inode_get_atime(base_vi)); | |
| vi->i_generation = base_vi->i_generation; | |
| } | |
| /* | |
| * ntfs_stream_update_base_time() - Update base timestamps after a stream change | |
| * @base_ni: base inode containing the stream attribute | |
| * | |
| * Update the base inode's mtime and ctime and mark it dirty. The helper takes | |
| * the base inode lock. | |
| */ | |
| void ntfs_stream_update_base_time(struct ntfs_inode *base_ni) | |
| { | |
| struct inode *base_vi = VFS_I(base_ni); | |
| inode_lock_nested(base_vi, I_MUTEX_PARENT); | |
| inode_set_mtime_to_ts(base_vi, inode_set_ctime_current(base_vi)); | |
| mark_inode_dirty(base_vi); | |
| inode_unlock(base_vi); | |
| } | |
| /* | |
| * ntfs_stream_inode_validate() - Validate a named-stream inode | |
| * @vi: stream inode to validate | |
| * | |
| * Verify that @vi still maps to its named DATA attribute and refresh its | |
| * base-derived metadata. The caller must hold the base inode's MFT-record | |
| * lock. | |
| * | |
| * Return: 0 if valid, or a negative errno. | |
| */ | |
| int ntfs_stream_inode_validate(struct inode *vi) | |
| { | |
| struct ntfs_inode *ni = NTFS_I(vi); | |
| struct ntfs_inode *base_ni; | |
| struct ntfs_attr_search_ctx *ctx; | |
| int err; | |
| if (!ntfs_inode_is_named_stream(ni) || ni->nr_extents != -1) | |
| return -EINVAL; | |
| base_ni = ni->ext.base_ntfs_ino; | |
| lockdep_assert_held(&base_ni->mrec_lock); | |
| if (NVolShutdown(base_ni->vol)) | |
| return -EIO; | |
| if (!NInoStreamUnlinked(ni) && | |
| (!vi->i_nlink || !VFS_I(base_ni)->i_nlink || | |
| NInoBeingDeleted(base_ni))) | |
| return -ESTALE; | |
| ctx = ntfs_attr_get_search_ctx(base_ni, NULL); | |
| if (!ctx) | |
| return -ENOMEM; | |
| err = ntfs_attr_lookup(AT_DATA, ni->name, ni->name_len, | |
| CASE_SENSITIVE, 0, NULL, 0, ctx); | |
| ntfs_attr_put_search_ctx(ctx); | |
| if (err) | |
| return err; | |
| ntfs_stream_inode_refresh(vi); | |
| return 0; | |
| } | |
| /* | |
| * ntfs_stream_unlinked() - Check a cached stream's deferred-unlink state | |
| * @base_ni: base inode containing the stream | |
| * @name: UTF-16LE stream name | |
| * @name_len: Length of @name in UTF-16 code units | |
| * | |
| * The caller must hold the base inode's MFT-record lock. | |
| * | |
| * Return: true if the cached stream inode is unlinked, or false if it is not | |
| * cached or is still linked. | |
| */ | |
| bool ntfs_stream_unlinked(struct ntfs_inode *base_ni, | |
| const __le16 *name, u32 name_len) | |
| { | |
| struct ntfs_attr na = { | |
| .mft_no = base_ni->mft_no, | |
| .type = AT_DATA, | |
| .name = (__le16 *)name, | |
| .name_len = name_len, | |
| }; | |
| struct inode *vi; | |
| bool unlinked; | |
| lockdep_assert_held(&base_ni->mrec_lock); | |
| rcu_read_lock(); | |
| vi = find_inode_rcu(base_ni->vol->sb, na.mft_no, | |
| ntfs_test_inode_rcu, &na); | |
| if (!vi) { | |
| rcu_read_unlock(); | |
| return false; | |
| } | |
| unlinked = NInoStreamUnlinked(NTFS_I(vi)); | |
| rcu_read_unlock(); | |
| return unlinked; | |
| } | |
| /* | |
| * ntfs_stream_path_parse() - Split a pathname stream component | |
| * @vol: NTFS volume | |
| * @qname: final pathname component | |
| * @path: receives slices of @qname on success | |
| * | |
| * Parse the ``file:stream`` form when the Windows pathname interface is | |
| * enabled. This does not convert or validate the component names. | |
| * | |
| * Return: 1 if stream syntax was parsed, 0 if streams are disabled or no | |
| * stream separator is present, or -EINVAL for malformed syntax. | |
| */ | |
| int ntfs_stream_path_parse(struct ntfs_volume *vol, | |
| const struct qstr *qname, struct ntfs_stream_path *path) | |
| { | |
| const unsigned char *colon; | |
| if (!NVolStreamsWindows(vol)) | |
| return 0; | |
| colon = memchr(qname->name, ':', qname->len); | |
| if (!colon) | |
| return 0; | |
| if (colon == qname->name || colon + 1 == qname->name + qname->len) | |
| return -EINVAL; | |
| if (memchr(colon + 1, ':', qname->name + qname->len - colon - 1)) | |
| return -EINVAL; | |
| path->base_name = (const char *)qname->name; | |
| path->base_len = colon - qname->name; | |
| path->stream_name = (const char *)colon + 1; | |
| path->stream_len = qname->name + qname->len - colon - 1; | |
| return 1; | |
| } | |
| /* | |
| * ntfs_stream_path_has_colon() - Check for a pathname stream separator | |
| * @vol: NTFS volume | |
| * @qname: final pathname component | |
| * | |
| * Return: true if the Windows pathname interface is enabled and @qname | |
| * contains a colon, or false otherwise. | |
| */ | |
| bool ntfs_stream_path_has_colon(struct ntfs_volume *vol, | |
| const struct qstr *qname) | |
| { | |
| return NVolStreamsWindows(vol) && | |
| memchr(qname->name, ':', qname->len); | |
| } | |
| /* | |
| * ntfs_stream_path_check() - Reject stream syntax for unsupported operations | |
| * @vol: NTFS volume | |
| * @qname: final pathname component | |
| * | |
| * Return: 0 for an ordinary name or when streams are disabled, | |
| * -EOPNOTSUPP for parsed stream syntax, or -EINVAL for malformed syntax. | |
| */ | |
| int ntfs_stream_path_check(struct ntfs_volume *vol, | |
| const struct qstr *qname) | |
| { | |
| struct ntfs_stream_path path; | |
| int ret; | |
| ret = ntfs_stream_path_parse(vol, qname, &path); | |
| if (ret < 0) | |
| return ret; | |
| return ret ? -EOPNOTSUPP : 0; | |
| } | |
| /* | |
| * ntfs_stream_lookup_inode_by_name() - Look up a base inode by name | |
| * @dir_ino: directory containing the base file or directory | |
| * @uname: UTF-16LE base name | |
| * @uname_len: Length of @uname in UTF-16 code units | |
| * | |
| * Resolve the directory entry and verify its MFT reference before returning | |
| * the inode. | |
| * | |
| * Return: Referenced inode on success, or ERR_PTR() on failure. | |
| */ | |
| struct inode *ntfs_stream_lookup_inode_by_name(struct inode *dir_ino, | |
| __le16 *uname, int uname_len) | |
| { | |
| struct ntfs_volume *vol = NTFS_SB(dir_ino->i_sb); | |
| struct ntfs_name *name = NULL; | |
| struct inode *inode; | |
| u64 mref; | |
| mutex_lock(&NTFS_I(dir_ino)->mrec_lock); | |
| mref = ntfs_lookup_inode_by_name(NTFS_I(dir_ino), uname, uname_len, | |
| &name); | |
| mutex_unlock(&NTFS_I(dir_ino)->mrec_lock); | |
| kfree(name); | |
| if (IS_ERR_MREF(mref)) | |
| return ERR_PTR(MREF_ERR(mref)); | |
| inode = ntfs_iget(vol->sb, MREF(mref)); | |
| if (IS_ERR(inode)) | |
| return inode; | |
| if (MSEQNO(mref) != NTFS_I(inode)->seq_no && | |
| MREF(mref) != FILE_MFT) { | |
| iput(inode); | |
| return ERR_PTR(-EIO); | |
| } | |
| return inode; | |
| } | |
| /* | |
| * Recheck that a stream dentry still names the same base inode and DATA | |
| * attribute. | |
| */ | |
| static int ntfs_stream_d_revalidate(struct inode *dir, | |
| const struct qstr *qname, struct dentry *dentry, | |
| unsigned int flags) | |
| { | |
| struct inode *inode = d_inode(dentry); | |
| struct ntfs_inode *ni; | |
| struct ntfs_stream_path path; | |
| struct inode *base_vi; | |
| __le16 *base_name = NULL, *stream_name = NULL; | |
| int base_len, stream_len, err, ret = 0; | |
| if (flags & LOOKUP_RCU) | |
| return -ECHILD; | |
| if (!inode) | |
| return 0; | |
| if (!inode->i_nlink) | |
| return 0; | |
| if (NVolShutdown(NTFS_SB(dir->i_sb))) | |
| return 0; | |
| ni = NTFS_I(inode); | |
| if (!ntfs_inode_is_named_stream(ni)) | |
| return 0; | |
| err = ntfs_stream_path_parse(NTFS_SB(dir->i_sb), qname, &path); | |
| if (err <= 0) | |
| return 0; | |
| base_len = ntfs_nlstoucs(NTFS_SB(dir->i_sb), path.base_name, | |
| path.base_len, &base_name, NTFS_MAX_NAME_LEN); | |
| if (base_len < 0) | |
| goto out; | |
| stream_len = ntfs_nlstoucs(NTFS_SB(dir->i_sb), path.stream_name, | |
| path.stream_len, &stream_name, NTFS_MAX_NAME_LEN); | |
| if (stream_len < 0) | |
| goto out; | |
| if (ntfs_check_stream_name(stream_name, stream_len)) | |
| goto out; | |
| if (stream_len != ni->name_len || | |
| !ntfs_names_are_equal(stream_name, stream_len, ni->name, | |
| ni->name_len, IGNORE_CASE, ni->vol->upcase, | |
| ni->vol->upcase_len)) | |
| goto out; | |
| base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len); | |
| if (IS_ERR(base_vi)) | |
| goto out; | |
| if (NTFS_I(base_vi) != ni->ext.base_ntfs_ino) { | |
| iput(base_vi); | |
| goto out; | |
| } | |
| mutex_lock(&NTFS_I(base_vi)->mrec_lock); | |
| err = ntfs_stream_inode_validate(inode); | |
| mutex_unlock(&NTFS_I(base_vi)->mrec_lock); | |
| iput(base_vi); | |
| if (!err) | |
| ret = 1; | |
| out: | |
| if (stream_name) | |
| kmem_cache_free(ntfs_name_cache, stream_name); | |
| if (base_name) | |
| kmem_cache_free(ntfs_name_cache, base_name); | |
| return ret; | |
| } | |
| /* Revalidate stream paths while leaving ordinary NTFS dentries cacheable. */ | |
| static int ntfs_dentry_revalidate(struct inode *dir, | |
| const struct qstr *qname, struct dentry *dentry, | |
| unsigned int flags) | |
| { | |
| struct inode *inode = d_inode(dentry); | |
| struct ntfs_volume *vol = NTFS_SB(dir->i_sb); | |
| if (ntfs_stream_path_has_colon(vol, qname)) | |
| return ntfs_stream_d_revalidate(dir, qname, dentry, flags); | |
| if (inode && ntfs_inode_is_named_stream(NTFS_I(inode))) | |
| return 0; | |
| return 1; | |
| } | |
| /* Drop stream-path aliases so future lookups recheck their backing attribute. */ | |
| static int ntfs_dentry_delete(const struct dentry *dentry) | |
| { | |
| struct inode *inode = d_inode(dentry); | |
| struct ntfs_volume *vol = NTFS_SB(dentry->d_sb); | |
| if (ntfs_stream_path_has_colon(vol, &dentry->d_name) || | |
| (inode && ntfs_inode_is_named_stream(NTFS_I(inode)))) | |
| return always_delete_dentry(dentry); | |
| return 0; | |
| } | |
| static const struct dentry_operations ntfs_dentry_ops = { | |
| .d_revalidate = ntfs_dentry_revalidate, | |
| .d_delete = ntfs_dentry_delete, | |
| }; | |
| /* | |
| * ntfs_set_default_dentry_ops() - Install stream-aware dentry operations | |
| * @sb: superblock on which to install the operations | |
| * | |
| * Ensure pathname stream dentries are revalidated and not kept as stale | |
| * aliases. | |
| */ | |
| void ntfs_set_default_dentry_ops(struct super_block *sb) | |
| { | |
| set_default_d_op(sb, &ntfs_dentry_ops); | |
| } | |
| /* | |
| * ntfs_lookup_stream() - Look up a named stream by pathname | |
| * @dir_ino: directory containing the base file or directory | |
| * @dent: dentry for the pathname component | |
| * @path: parsed base and stream name slices | |
| * | |
| * Find the existing base inode and its named DATA attribute, then splice the | |
| * stream inode into @dent. | |
| * | |
| * Return: NULL if @dent was instantiated, an alternate dentry if a | |
| * disconnected alias was spliced, or ERR_PTR() on failure. | |
| */ | |
| struct dentry *ntfs_lookup_stream(struct inode *dir_ino, | |
| struct dentry *dent, const struct ntfs_stream_path *path) | |
| { | |
| struct ntfs_volume *vol = NTFS_SB(dir_ino->i_sb); | |
| struct inode *base_vi, *stream_vi; | |
| __le16 *base_uname = NULL, *stream_uname = NULL; | |
| int base_len, stream_len, err; | |
| if (NVolShutdown(vol)) | |
| return ERR_PTR(-EIO); | |
| base_len = ntfs_nlstoucs(vol, path->base_name, path->base_len, | |
| &base_uname, NTFS_MAX_NAME_LEN); | |
| if (base_len < 0) | |
| return ERR_PTR(base_len); | |
| stream_len = ntfs_nlstoucs(vol, path->stream_name, path->stream_len, | |
| &stream_uname, NTFS_MAX_NAME_LEN); | |
| if (stream_len < 0) { | |
| kmem_cache_free(ntfs_name_cache, base_uname); | |
| return ERR_PTR(stream_len); | |
| } | |
| err = ntfs_check_stream_name(stream_uname, stream_len); | |
| if (err) { | |
| kmem_cache_free(ntfs_name_cache, base_uname); | |
| kmem_cache_free(ntfs_name_cache, stream_uname); | |
| return ERR_PTR(err); | |
| } | |
| base_vi = ntfs_stream_lookup_inode_by_name(dir_ino, base_uname, | |
| base_len); | |
| kmem_cache_free(ntfs_name_cache, base_uname); | |
| if (IS_ERR(base_vi)) { | |
| err = PTR_ERR(base_vi); | |
| kmem_cache_free(ntfs_name_cache, stream_uname); | |
| if (err == -ENOENT) | |
| return d_splice_alias(NULL, dent); | |
| return ERR_PTR(err); | |
| } | |
| if (!S_ISREG(base_vi->i_mode) && !S_ISDIR(base_vi->i_mode)) { | |
| iput(base_vi); | |
| kmem_cache_free(ntfs_name_cache, stream_uname); | |
| return ERR_PTR(-ENOTDIR); | |
| } | |
| mutex_lock(&NTFS_I(base_vi)->mrec_lock); | |
| stream_vi = ntfs_attr_iget(base_vi, AT_DATA, stream_uname, stream_len); | |
| if (!IS_ERR(stream_vi)) { | |
| if (NInoStreamUnlinked(NTFS_I(stream_vi))) | |
| err = -ENOENT; | |
| else | |
| err = ntfs_stream_inode_validate(stream_vi); | |
| } | |
| mutex_unlock(&NTFS_I(base_vi)->mrec_lock); | |
| if (!IS_ERR(stream_vi) && err) { | |
| iput(stream_vi); | |
| stream_vi = ERR_PTR(err); | |
| } | |
| iput(base_vi); | |
| kmem_cache_free(ntfs_name_cache, stream_uname); | |
| if (IS_ERR(stream_vi)) { | |
| err = PTR_ERR(stream_vi); | |
| if (err == -ENOENT || err == -ESTALE) | |
| return d_splice_alias(NULL, dent); | |
| return ERR_PTR(err); | |
| } | |
| return d_splice_alias(stream_vi, dent); | |
| } | |
| /* | |
| * ntfs_stream_path_names() - Convert and validate pathname stream names | |
| * @vol: NTFS volume | |
| * @qname: final pathname component | |
| * @base_name: receives the allocated UTF-16LE base name | |
| * @base_len: receives the base name length in UTF-16 code units | |
| * @stream_name: receives the allocated UTF-16LE stream name | |
| * @stream_len: receives the stream name length in UTF-16 code units | |
| * | |
| * Return: 1 if stream names were produced, 0 if @qname has no stream syntax, | |
| * or a negative errno. The caller owns both name buffers on success. | |
| */ | |
| int ntfs_stream_path_names(struct ntfs_volume *vol, | |
| const struct qstr *qname, __le16 **base_name, int *base_len, | |
| __le16 **stream_name, int *stream_len) | |
| { | |
| struct ntfs_stream_path path; | |
| int err; | |
| *base_name = NULL; | |
| *stream_name = NULL; | |
| err = ntfs_stream_path_parse(vol, qname, &path); | |
| if (err <= 0) | |
| return err; | |
| *base_len = ntfs_nlstoucs(vol, path.base_name, path.base_len, | |
| base_name, NTFS_MAX_NAME_LEN); | |
| if (*base_len < 0) | |
| return *base_len; | |
| *stream_len = ntfs_nlstoucs(vol, path.stream_name, path.stream_len, | |
| stream_name, NTFS_MAX_NAME_LEN); | |
| if (*stream_len < 0) { | |
| kmem_cache_free(ntfs_name_cache, *base_name); | |
| *base_name = NULL; | |
| return *stream_len; | |
| } | |
| err = ntfs_check_bad_windows_name(vol, *base_name, *base_len); | |
| if (err) | |
| goto out_free; | |
| err = ntfs_check_stream_name(*stream_name, *stream_len); | |
| if (err) | |
| goto out_free; | |
| err = ntfs_check_bad_windows_name(vol, *stream_name, *stream_len); | |
| if (err) | |
| goto out_free; | |
| return 1; | |
| out_free: | |
| kmem_cache_free(ntfs_name_cache, *stream_name); | |
| kmem_cache_free(ntfs_name_cache, *base_name); | |
| *stream_name = NULL; | |
| *base_name = NULL; | |
| return err; | |
| } | |
| /* | |
| * ntfs_create_named_stream() - Create a named DATA stream | |
| * @idmap: mount idmap used for permission checks | |
| * @dir: directory containing the base object | |
| * @base_name: UTF-16LE base name | |
| * @base_len: length of @base_name in UTF-16 code units | |
| * @stream_name: UTF-16LE stream name | |
| * @stream_len: length of @stream_name in UTF-16 code units | |
| * | |
| * Create the stream on an existing regular file or directory. | |
| * | |
| * Return: Referenced stream inode on success, or ERR_PTR() on failure. | |
| */ | |
| struct inode *ntfs_create_named_stream(const struct mnt_idmap *idmap, | |
| struct inode *dir, __le16 *base_name, int base_len, | |
| __le16 *stream_name, int stream_len) | |
| { | |
| struct ntfs_inode *base_ni; | |
| struct inode *base_vi, *stream_vi; | |
| struct inode *rollback_vi = NULL; | |
| struct ntfs_attr_search_ctx *ctx; | |
| bool stream_created = false; | |
| int err, rollback_err; | |
| base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len); | |
| if (IS_ERR(base_vi)) | |
| return base_vi; | |
| if (!S_ISREG(base_vi->i_mode) && !S_ISDIR(base_vi->i_mode)) { | |
| iput(base_vi); | |
| return ERR_PTR(-ENOTDIR); | |
| } | |
| err = inode_permission(idmap, base_vi, MAY_OPEN | MAY_WRITE); | |
| if (err) | |
| goto out_iput; | |
| if (IS_APPEND(base_vi) || IS_IMMUTABLE(base_vi)) { | |
| err = -EPERM; | |
| goto out_iput; | |
| } | |
| if (!(NTFS_SB(base_vi->i_sb)->vol_flags & VOLUME_IS_DIRTY)) { | |
| err = ntfs_set_volume_flags(NTFS_SB(base_vi->i_sb), | |
| VOLUME_IS_DIRTY); | |
| if (err) | |
| goto out_iput; | |
| } | |
| base_ni = NTFS_I(base_vi); | |
| mutex_lock(&base_ni->mrec_lock); | |
| if (NVolShutdown(base_ni->vol)) { | |
| err = -EIO; | |
| goto out_unlock; | |
| } | |
| if (NInoBeingDeleted(base_ni) || !base_vi->i_nlink) { | |
| err = -ENOENT; | |
| goto out_unlock; | |
| } | |
| if (IS_APPEND(base_vi) || IS_IMMUTABLE(base_vi)) { | |
| err = -EPERM; | |
| goto out_unlock; | |
| } | |
| ctx = ntfs_attr_get_search_ctx(base_ni, NULL); | |
| if (!ctx) { | |
| err = -ENOMEM; | |
| goto out_unlock; | |
| } | |
| err = ntfs_attr_lookup(AT_DATA, stream_name, stream_len, | |
| IGNORE_CASE, 0, NULL, 0, ctx); | |
| if (!err) { | |
| if (ntfs_stream_unlinked(base_ni, stream_name, stream_len)) | |
| err = -EBUSY; | |
| else | |
| err = -EEXIST; | |
| } else if (err == -ENOENT) { | |
| if (NVolShutdown(base_ni->vol)) { | |
| err = -EIO; | |
| goto out_put_ctx; | |
| } | |
| err = ntfs_attr_add(base_ni, AT_DATA, stream_name, stream_len, | |
| NULL, 0); | |
| if (!err) { | |
| stream_created = true; | |
| mark_mft_record_dirty(base_ni); | |
| } | |
| } | |
| ntfs_attr_put_search_ctx(ctx); | |
| if (err) | |
| goto out_unlock; | |
| stream_vi = ntfs_attr_iget(base_vi, AT_DATA, stream_name, stream_len); | |
| if (!IS_ERR(stream_vi)) | |
| err = ntfs_stream_inode_validate(stream_vi); | |
| else | |
| err = PTR_ERR(stream_vi); | |
| if (err) { | |
| if (stream_created) { | |
| rollback_err = ntfs_attr_remove(base_ni, AT_DATA, | |
| stream_name, stream_len, &rollback_vi); | |
| if (rollback_err) { | |
| ntfs_error(base_ni->vol->sb, | |
| "Failed to roll back named stream creation.\n"); | |
| if (rollback_err == -ENOMEM || | |
| rollback_err == -EINTR || | |
| rollback_err == -ERESTARTSYS) { | |
| err = rollback_err; | |
| } else { | |
| NVolSetErrors(base_ni->vol); | |
| NVolSetShutdown(base_ni->vol); | |
| err = -EIO; | |
| } | |
| } | |
| } | |
| mutex_unlock(&base_ni->mrec_lock); | |
| if (stream_created) | |
| ntfs_stream_update_base_time(base_ni); | |
| if (!IS_ERR(stream_vi)) | |
| iput(stream_vi); | |
| if (rollback_vi) | |
| iput(rollback_vi); | |
| iput(base_vi); | |
| return ERR_PTR(err); | |
| } | |
| mutex_unlock(&base_ni->mrec_lock); | |
| if (stream_created) | |
| ntfs_stream_update_base_time(base_ni); | |
| iput(base_vi); | |
| return stream_vi; | |
| out_put_ctx: | |
| ntfs_attr_put_search_ctx(ctx); | |
| out_unlock: | |
| mutex_unlock(&base_ni->mrec_lock); | |
| out_iput: | |
| iput(base_vi); | |
| return ERR_PTR(err); | |
| } | |
| /* | |
| * ntfs_unlink_named_stream() - Remove a pathname named stream | |
| * @dir: directory containing the base object | |
| * @dentry: dentry for the named stream | |
| * | |
| * Return: 0 on success, or a negative errno. | |
| */ | |
| int ntfs_unlink_named_stream(struct inode *dir, struct dentry *dentry) | |
| { | |
| struct ntfs_volume *vol = NTFS_SB(dir->i_sb); | |
| struct inode *base_vi; | |
| __le16 *base_name = NULL, *stream_name = NULL; | |
| int base_len, stream_len, path_result, err; | |
| path_result = ntfs_stream_path_names(vol, &dentry->d_name, &base_name, | |
| &base_len, &stream_name, &stream_len); | |
| if (path_result < 0) | |
| return path_result; | |
| if (!path_result) | |
| return -EINVAL; | |
| base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len); | |
| if (IS_ERR(base_vi)) { | |
| err = PTR_ERR(base_vi); | |
| goto out_free; | |
| } | |
| if (!S_ISREG(base_vi->i_mode) && !S_ISDIR(base_vi->i_mode)) { | |
| err = -ENOTDIR; | |
| goto out_iput; | |
| } | |
| if (!ntfs_inode_is_named_stream(NTFS_I(dentry->d_inode))) { | |
| err = -ESTALE; | |
| goto out_iput; | |
| } | |
| if (NTFS_I(dentry->d_inode)->ext.base_ntfs_ino != NTFS_I(base_vi)) { | |
| err = -ESTALE; | |
| goto out_iput; | |
| } | |
| /* | |
| * VFS authorizes pathname removal via the parent directory, not base | |
| * file write access. The shared helper enforces NTFS delete restrictions. | |
| */ | |
| err = ntfs_remove_named_stream(NTFS_I(base_vi), stream_name, | |
| stream_len, dentry->d_inode); | |
| out_iput: | |
| iput(base_vi); | |
| out_free: | |
| kmem_cache_free(ntfs_name_cache, stream_name); | |
| kmem_cache_free(ntfs_name_cache, base_name); | |
| return err; | |
| } | |
| /* Obtain a dentry for the base inode behind a stream inode. */ | |
| static struct dentry *ntfs_stream_base_dentry(struct inode *inode) | |
| { | |
| struct inode *base_vi = | |
| VFS_I(NTFS_I(inode)->ext.base_ntfs_ino); | |
| struct dentry *dentry; | |
| dentry = d_find_alias(base_vi); | |
| if (dentry) | |
| return dentry; | |
| if (!igrab(base_vi)) | |
| return ERR_PTR(-ESTALE); | |
| return d_obtain_alias(base_vi); | |
| } | |
| /* | |
| * ntfs_stream_validate_for_mutation() - Validate a stream before mutation | |
| * @inode: stream inode to validate | |
| * @nowait: do not wait for the base MFT-record lock | |
| * | |
| * Return: 0 if the backing DATA attribute is valid, -EAGAIN if a nonblocking | |
| * lock attempt fails, or another negative errno. | |
| */ | |
| int ntfs_stream_validate_for_mutation(struct inode *inode, bool nowait) | |
| { | |
| struct ntfs_inode *base_ni = NTFS_I(inode)->ext.base_ntfs_ino; | |
| int err; | |
| if (nowait) { | |
| if (!mutex_trylock(&base_ni->mrec_lock)) | |
| return -EAGAIN; | |
| } else { | |
| mutex_lock(&base_ni->mrec_lock); | |
| } | |
| err = ntfs_stream_inode_validate(inode); | |
| mutex_unlock(&base_ni->mrec_lock); | |
| return err; | |
| } | |
| /* | |
| * Hold a temporary stream reference so unlink cannot remove its attribute | |
| * while an operation is using it. | |
| */ | |
| static int ntfs_stream_claim(struct inode *inode) | |
| { | |
| struct ntfs_inode *ni = NTFS_I(inode); | |
| struct ntfs_inode *base_ni = ni->ext.base_ntfs_ino; | |
| int err; | |
| mutex_lock(&base_ni->mrec_lock); | |
| if (NInoStreamUnlinked(ni)) | |
| err = -ESTALE; | |
| else | |
| err = ntfs_stream_inode_validate(inode); | |
| if (!err) { | |
| if (atomic_read(&ni->stream_open_count) < 0) | |
| err = -ESTALE; | |
| else | |
| atomic_inc(&ni->stream_open_count); | |
| } | |
| mutex_unlock(&base_ni->mrec_lock); | |
| return err; | |
| } | |
| /* Stream permission checks use the base inode's permissions. */ | |
| static int ntfs_stream_permission(const struct mnt_idmap *idmap, | |
| struct inode *inode, int mask) | |
| { | |
| return inode_permission(idmap, | |
| VFS_I(NTFS_I(inode)->ext.base_ntfs_ino), mask); | |
| } | |
| /* Report base metadata with the stream's own size and allocation. */ | |
| static int ntfs_stream_getattr(const struct mnt_idmap *idmap, | |
| const struct path *path, struct kstat *stat, | |
| unsigned int request_mask, unsigned int query_flags) | |
| { | |
| struct inode *inode = d_backing_inode(path->dentry); | |
| struct ntfs_inode *ni = NTFS_I(inode); | |
| struct inode *base_vi = VFS_I(ni->ext.base_ntfs_ino); | |
| generic_fillattr(idmap, request_mask, base_vi, stat); | |
| stat->size = i_size_read(inode); | |
| stat->blocks = (((u64)ni->i_dealloc_clusters << | |
| NTFS_SB(inode->i_sb)->cluster_size_bits) >> 9) + | |
| inode->i_blocks; | |
| stat->blksize = NTFS_SB(inode->i_sb)->cluster_size; | |
| stat->result_mask |= STATX_BTIME; | |
| stat->btime = NTFS_I(base_vi)->i_crtime; | |
| if (NInoCompressed(ni)) | |
| stat->attributes |= STATX_ATTR_COMPRESSED; | |
| if (NInoEncrypted(ni)) | |
| stat->attributes |= STATX_ATTR_ENCRYPTED; | |
| if (base_vi->i_flags & S_IMMUTABLE) | |
| stat->attributes |= STATX_ATTR_IMMUTABLE; | |
| if (base_vi->i_flags & S_APPEND) | |
| stat->attributes |= STATX_ATTR_APPEND; | |
| stat->attributes_mask |= STATX_ATTR_COMPRESSED | STATX_ATTR_ENCRYPTED | | |
| STATX_ATTR_IMMUTABLE | STATX_ATTR_APPEND; | |
| stat->mode = (stat->mode & ~S_IFMT) | S_IFREG; | |
| if (request_mask & STATX_DIOALIGN) { | |
| unsigned int align = | |
| bdev_logical_block_size(inode->i_sb->s_bdev); | |
| stat->result_mask |= STATX_DIOALIGN; | |
| if (!NInoCompressed(ni) && !NInoEncrypted(ni)) { | |
| stat->dio_mem_align = align; | |
| stat->dio_offset_align = align; | |
| } | |
| } | |
| return 0; | |
| } | |
| /* | |
| * Apply size changes to the stream and route shared inode metadata changes to | |
| * its base inode. | |
| */ | |
| static int ntfs_stream_setattr_common(const struct mnt_idmap *idmap, | |
| struct inode *inode, struct iattr *attr) | |
| { | |
| struct ntfs_inode *ni = NTFS_I(inode); | |
| struct inode *base_vi = VFS_I(ni->ext.base_ntfs_ino); | |
| struct dentry *base_dentry = NULL; | |
| struct iattr base_attr = *attr; | |
| int err; | |
| base_attr.ia_valid &= ~ATTR_FILE; | |
| base_attr.ia_file = NULL; | |
| if (base_attr.ia_valid & (ATTR_KILL_SUID | ATTR_KILL_SGID)) | |
| base_attr.ia_valid &= ~ATTR_MODE; | |
| else if (base_attr.ia_valid & ATTR_MODE) | |
| base_attr.ia_mode = (base_attr.ia_mode & ~S_IFMT) | | |
| (base_vi->i_mode & S_IFMT); | |
| if (attr->ia_valid & ATTR_SIZE) { | |
| err = inode_permission(idmap, base_vi, MAY_WRITE); | |
| if (err) | |
| goto out; | |
| if (IS_APPEND(base_vi) || IS_IMMUTABLE(base_vi)) { | |
| err = -EPERM; | |
| goto out; | |
| } | |
| if (!(ni->vol->vol_flags & VOLUME_IS_DIRTY)) { | |
| err = ntfs_set_volume_flags(ni->vol, VOLUME_IS_DIRTY); | |
| if (err) | |
| goto out; | |
| } | |
| base_attr.ia_valid &= ~ATTR_SIZE; | |
| } | |
| if ((attr->ia_valid & ATTR_SIZE) || base_attr.ia_valid) { | |
| base_dentry = ntfs_stream_base_dentry(inode); | |
| if (IS_ERR(base_dentry)) { | |
| err = PTR_ERR(base_dentry); | |
| base_dentry = NULL; | |
| goto out; | |
| } | |
| inode_lock_nested(base_vi, I_MUTEX_PARENT); | |
| err = ntfs_stream_validate_for_mutation(inode, false); | |
| if (err) { | |
| inode_unlock(base_vi); | |
| goto out; | |
| } | |
| if (base_attr.ia_valid) | |
| err = notify_change(idmap, base_dentry, &base_attr, | |
| NULL); | |
| else | |
| err = 0; | |
| if (!err) | |
| ntfs_stream_inode_refresh(inode); | |
| inode_unlock(base_vi); | |
| if (err) | |
| goto out; | |
| } | |
| if (attr->ia_valid & ATTR_SIZE) | |
| err = ntfs_setattr_size(inode, attr); | |
| else | |
| err = 0; | |
| out: | |
| dput(base_dentry); | |
| return err; | |
| } | |
| /* Hold the stream against unlink while applying VFS setattr operations. */ | |
| static int ntfs_stream_setattr(const struct mnt_idmap *idmap, | |
| struct dentry *dentry, struct iattr *attr) | |
| { | |
| struct inode *inode = d_inode(dentry); | |
| struct ntfs_inode *ni = NTFS_I(inode); | |
| bool claimed = false; | |
| int err; | |
| if (NVolShutdown(ni->vol)) | |
| return -EIO; | |
| if (!(attr->ia_valid & ATTR_FILE)) { | |
| err = ntfs_stream_claim(inode); | |
| if (err) | |
| return err; | |
| claimed = true; | |
| } | |
| err = ntfs_stream_setattr_common(idmap, inode, attr); | |
| if (claimed) | |
| ntfs_stream_put(inode); | |
| return err; | |
| } | |
| /* | |
| * Apply stream timestamp updates to the base inode after validating the | |
| * backing attribute under its MFT-record lock. Nonblocking callers get | |
| * -EAGAIN. | |
| */ | |
| static int ntfs_stream_update_time_common(struct inode *inode, | |
| enum fs_update_time type, unsigned int flags) | |
| { | |
| struct ntfs_inode *base_ni = NTFS_I(inode)->ext.base_ntfs_ino; | |
| struct inode *base_vi = VFS_I(base_ni); | |
| int err; | |
| if (NVolShutdown(base_ni->vol)) | |
| return -EIO; | |
| if (flags & IOCB_NOWAIT) | |
| return -EAGAIN; | |
| mutex_lock(&base_ni->mrec_lock); | |
| if (NVolShutdown(base_ni->vol)) { | |
| err = -EIO; | |
| goto out_mrec; | |
| } | |
| err = ntfs_stream_inode_validate(inode); | |
| if (!err) { | |
| err = generic_update_time(base_vi, type, flags); | |
| if (!err) | |
| ntfs_stream_inode_refresh(inode); | |
| } | |
| out_mrec: | |
| mutex_unlock(&base_ni->mrec_lock); | |
| return err; | |
| } | |
| static int ntfs_stream_update_time(struct inode *inode, | |
| enum fs_update_time type, unsigned int flags) | |
| { | |
| return ntfs_stream_update_time_common(inode, type, flags); | |
| } | |
| static ssize_t ntfs_stream_listxattr(struct dentry *dentry, char *buffer, | |
| size_t size) | |
| { | |
| return -EOPNOTSUPP; | |
| } | |
| #ifdef CONFIG_NTFS_FS_POSIX_ACL | |
| static struct posix_acl *ntfs_stream_get_acl(const struct mnt_idmap *idmap, | |
| struct dentry *dentry, int type) | |
| { | |
| return ERR_PTR(-EOPNOTSUPP); | |
| } | |
| static int ntfs_stream_set_acl(const struct mnt_idmap *idmap, | |
| struct dentry *dentry, struct posix_acl *acl, int type) | |
| { | |
| return -EOPNOTSUPP; | |
| } | |
| #endif | |
| /* | |
| * Remove an unlinked stream's DATA attribute after its final active reference | |
| * is released. Base-inode deletion handles the attribute when the base is | |
| * already being removed. | |
| */ | |
| static int ntfs_stream_finish_remove(struct inode *attr_vi) | |
| { | |
| struct ntfs_inode *attr_ni = NTFS_I(attr_vi); | |
| struct ntfs_inode *ni = attr_ni->ext.base_ntfs_ino; | |
| int err; | |
| mutex_lock(&ni->mrec_lock); | |
| if (!NInoStreamUnlinked(attr_ni) || | |
| atomic_cmpxchg(&attr_ni->stream_open_count, 0, -1)) { | |
| err = 0; | |
| goto out_unlock; | |
| } | |
| if (NInoBeingDeleted(ni) || !VFS_I(ni)->i_nlink) { | |
| remove_inode_hash(attr_vi); | |
| err = 0; | |
| goto out_unlock; | |
| } | |
| mutex_unlock(&ni->mrec_lock); | |
| truncate_inode_pages(attr_vi->i_mapping, 0); | |
| mutex_lock(&ni->mrec_lock); | |
| if (NVolShutdown(ni->vol)) { | |
| err = -EIO; | |
| goto out_unlock; | |
| } | |
| if (NInoBeingDeleted(ni) || !VFS_I(ni)->i_nlink) { | |
| remove_inode_hash(attr_vi); | |
| err = 0; | |
| goto out_unlock; | |
| } | |
| err = ntfs_attr_rm(attr_ni); | |
| if (!err) { | |
| NInoClearDirty(attr_ni); | |
| remove_inode_hash(attr_vi); | |
| } else { | |
| NInoSetBeingDeleted(attr_ni); | |
| remove_inode_hash(attr_vi); | |
| } | |
| out_unlock: | |
| mutex_unlock(&ni->mrec_lock); | |
| return err; | |
| } | |
| /* | |
| * ntfs_stream_put() - Release a stream operation reference | |
| * @vi: stream inode whose reference is being released | |
| * | |
| * Finish a deferred unlink when this is the last reference to an unlinked | |
| * stream. | |
| * | |
| * Return: 0 on success, or a negative errno if deferred removal fails. | |
| */ | |
| int ntfs_stream_put(struct inode *vi) | |
| { | |
| struct ntfs_inode *ni = NTFS_I(vi); | |
| if (atomic_dec_and_test(&ni->stream_open_count) && | |
| NInoStreamUnlinked(ni)) | |
| return ntfs_stream_finish_remove(vi); | |
| return 0; | |
| } | |
| /* | |
| * ntfs_remove_named_stream() - Remove a named DATA stream | |
| * @ni: base inode containing the stream | |
| * @uname: UTF-16LE stream name | |
| * @uname_len: length of @uname in UTF-16 code units | |
| * @expected_vi: expected cached stream inode, or NULL | |
| * | |
| * Refuse removal while the stream is open. If @expected_vi is non-NULL, it | |
| * must be the inode currently associated with the named attribute. | |
| * | |
| * Return: 0 on success, -ENOENT if the stream is absent, or another negative | |
| * errno. | |
| */ | |
| int ntfs_remove_named_stream(struct ntfs_inode *ni, __le16 *uname, | |
| u32 uname_len, struct inode *expected_vi) | |
| { | |
| struct inode *attr_vi; | |
| struct ntfs_inode *attr_ni; | |
| int err; | |
| if (!ni || !uname || uname_len == 0) | |
| return -EINVAL; | |
| if (NVolShutdown(ni->vol)) | |
| return -EIO; | |
| if ((ni->flags & FILE_ATTR_READONLY) || | |
| IS_APPEND(VFS_I(ni)) || IS_IMMUTABLE(VFS_I(ni))) | |
| return -EPERM; | |
| if (!(ni->vol->vol_flags & VOLUME_IS_DIRTY)) { | |
| err = ntfs_set_volume_flags(ni->vol, VOLUME_IS_DIRTY); | |
| if (err) | |
| return err; | |
| } | |
| mutex_lock(&ni->mrec_lock); | |
| if (NInoBeingDeleted(ni) || !VFS_I(ni)->i_nlink) { | |
| err = -ENOENT; | |
| goto out_unlock; | |
| } | |
| attr_vi = ntfs_attr_iget(VFS_I(ni), AT_DATA, uname, uname_len); | |
| if (IS_ERR(attr_vi)) { | |
| err = PTR_ERR(attr_vi); | |
| goto out_unlock; | |
| } | |
| if (expected_vi && attr_vi != expected_vi) { | |
| err = -ESTALE; | |
| goto out_iput; | |
| } | |
| attr_ni = NTFS_I(attr_vi); | |
| if (NInoStreamUnlinked(attr_ni)) { | |
| err = -ENOENT; | |
| goto out_iput; | |
| } | |
| if (atomic_read(&attr_ni->stream_open_count) > 0) { | |
| err = -EBUSY; | |
| goto out_iput; | |
| } | |
| err = ntfs_stream_inode_validate(attr_vi); | |
| if (err) | |
| goto out_iput; | |
| NInoSetStreamUnlinked(attr_ni); | |
| clear_nlink(attr_vi); | |
| mutex_unlock(&ni->mrec_lock); | |
| err = ntfs_stream_finish_remove(attr_vi); | |
| if (!err) | |
| ntfs_stream_update_base_time(ni); | |
| iput(attr_vi); | |
| return err; | |
| out_iput: | |
| mutex_unlock(&ni->mrec_lock); | |
| iput(attr_vi); | |
| return err; | |
| out_unlock: | |
| mutex_unlock(&ni->mrec_lock); | |
| return err; | |
| } | |
| /* | |
| * Validate access against the base inode and hold the stream against unlink | |
| * until the file is released. | |
| */ | |
| static int ntfs_stream_file_open(struct inode *inode, struct file *file) | |
| { | |
| struct ntfs_inode *ni = NTFS_I(inode); | |
| struct ntfs_inode *base_ni = ni->ext.base_ntfs_ino; | |
| struct inode *base_vi = VFS_I(base_ni); | |
| int mask = MAY_OPEN; | |
| int err; | |
| if (file->f_mode & FMODE_READ) | |
| mask |= MAY_READ; | |
| if (file->f_mode & FMODE_WRITE) | |
| mask |= MAY_WRITE; | |
| err = inode_permission(file_mnt_idmap(file), base_vi, mask); | |
| if (err) | |
| return err; | |
| mutex_lock(&base_ni->mrec_lock); | |
| if (NInoStreamUnlinked(ni)) | |
| err = -ENOENT; | |
| else | |
| err = ntfs_stream_inode_validate(inode); | |
| if (err) | |
| goto out_unlock; | |
| if ((file->f_mode & FMODE_WRITE) && | |
| (IS_IMMUTABLE(base_vi) || | |
| (IS_APPEND(base_vi) && !(file->f_flags & O_APPEND)))) { | |
| err = -EPERM; | |
| goto out_unlock; | |
| } | |
| if ((file->f_flags & O_TRUNC) && IS_APPEND(base_vi)) { | |
| err = -EPERM; | |
| goto out_unlock; | |
| } | |
| if ((file->f_flags & O_NOATIME) && | |
| !inode_owner_or_capable(file_mnt_idmap(file), base_vi)) { | |
| err = -EPERM; | |
| goto out_unlock; | |
| } | |
| err = ntfs_file_open(inode, file); | |
| if (err) | |
| goto out_unlock; | |
| if (file->f_mode & FMODE_WRITE) { | |
| err = get_write_access(base_vi); | |
| if (err) | |
| goto out_unlock; | |
| file->private_data = base_vi; | |
| } | |
| atomic_inc(&ni->stream_open_count); | |
| out_unlock: | |
| mutex_unlock(&base_ni->mrec_lock); | |
| return err; | |
| } | |
| /* Drop open-time references and complete any deferred stream unlink. */ | |
| static int ntfs_stream_file_release(struct inode *inode, struct file *file) | |
| { | |
| int err, remove_err; | |
| err = ntfs_file_release(inode, file); | |
| if (file->private_data) { | |
| put_write_access(file->private_data); | |
| file->private_data = NULL; | |
| } | |
| remove_err = ntfs_stream_put(inode); | |
| if (!err) | |
| err = remove_err; | |
| return err; | |
| } | |
| enum ntfs_stream_ioctl_op { | |
| NTFS_STREAM_IOCTL_READ, | |
| NTFS_STREAM_IOCTL_WRITE, | |
| NTFS_STREAM_IOCTL_REMOVE, | |
| }; | |
| /* | |
| * Look up or create an ioctl target stream and return its referenced inode. | |
| */ | |
| static int ntfs_stream_ioctl_get_inode(struct inode *base_vi, __le16 *sname, | |
| int sname_len, bool create, struct inode **stream_vi) | |
| { | |
| struct ntfs_inode *base_ni = NTFS_I(base_vi); | |
| struct ntfs_attr_search_ctx *ctx; | |
| struct inode *attr_vi = NULL; | |
| bool created = false; | |
| int err; | |
| *stream_vi = NULL; | |
| mutex_lock(&base_ni->mrec_lock); | |
| if (NVolShutdown(base_ni->vol)) { | |
| err = -EIO; | |
| goto out_unlock; | |
| } | |
| if (NInoBeingDeleted(base_ni) || !base_vi->i_nlink) { | |
| err = -ENOENT; | |
| goto out_unlock; | |
| } | |
| ctx = ntfs_attr_get_search_ctx(base_ni, NULL); | |
| if (!ctx) { | |
| err = -ENOMEM; | |
| goto out_unlock; | |
| } | |
| err = ntfs_attr_lookup(AT_DATA, sname, sname_len, IGNORE_CASE, | |
| 0, NULL, 0, ctx); | |
| if (err == -ENOENT && create) { | |
| err = ntfs_attr_add(base_ni, AT_DATA, sname, sname_len, | |
| NULL, 0); | |
| if (!err) { | |
| created = true; | |
| mark_mft_record_dirty(base_ni); | |
| } | |
| } | |
| ntfs_attr_put_search_ctx(ctx); | |
| if (err) | |
| goto out_unlock; | |
| attr_vi = ntfs_attr_iget(base_vi, AT_DATA, sname, sname_len); | |
| if (IS_ERR(attr_vi)) { | |
| err = PTR_ERR(attr_vi); | |
| attr_vi = NULL; | |
| goto out_unlock; | |
| } | |
| if (NInoStreamUnlinked(NTFS_I(attr_vi))) | |
| err = create ? -EBUSY : -ENOENT; | |
| else | |
| err = ntfs_stream_inode_validate(attr_vi); | |
| out_unlock: | |
| mutex_unlock(&base_ni->mrec_lock); | |
| if (created) | |
| ntfs_stream_update_base_time(base_ni); | |
| if (err) { | |
| iput(attr_vi); | |
| return err; | |
| } | |
| *stream_vi = attr_vi; | |
| return 0; | |
| } | |
| /* Check file mode, base-inode flags, and the requested transfer range. */ | |
| static int ntfs_stream_ioctl_access(struct file *filp, | |
| enum ntfs_stream_ioctl_op op, loff_t pos, size_t len) | |
| { | |
| struct inode *inode = file_inode(filp); | |
| bool is_dir = S_ISDIR(inode->i_mode); | |
| int err; | |
| if (op == NTFS_STREAM_IOCTL_READ) { | |
| if (!(filp->f_mode & FMODE_READ)) | |
| return -EBADF; | |
| return rw_verify_area(READ, filp, &pos, len); | |
| } | |
| if (!(filp->f_mode & FMODE_WRITE) && !is_dir) | |
| return -EBADF; | |
| if (is_dir) { | |
| err = inode_permission(file_mnt_idmap(filp), inode, | |
| MAY_WRITE | MAY_EXEC); | |
| if (err) | |
| return err; | |
| } | |
| if (IS_APPEND(inode) || IS_IMMUTABLE(inode)) | |
| return -EPERM; | |
| if (op == NTFS_STREAM_IOCTL_REMOVE) | |
| return 0; | |
| return rw_verify_area(WRITE, filp, &pos, len); | |
| } | |
| /* | |
| * Validate and execute a named-stream read, write, or remove ioctl request. | |
| */ | |
| static long ntfs_ioctl_stream(struct file *filp, unsigned long arg, | |
| enum ntfs_stream_ioctl_op op) | |
| { | |
| struct inode *base_vi = file_inode(filp); | |
| struct ntfs_inode *base_ni = NTFS_I(base_vi); | |
| struct ntfs_stream hdr; | |
| struct ntfs_stream *req; | |
| struct inode *stream_vi = NULL; | |
| __le16 *uname = NULL, *sname; | |
| size_t data_size, total_size; | |
| loff_t pos; | |
| s64 ret; | |
| int sname_len, err; | |
| bool claimed = false, got_write = false, utf16; | |
| if (NVolShutdown(base_ni->vol)) | |
| return -EIO; | |
| if (NInoAttr(base_ni) || | |
| (!S_ISREG(base_vi->i_mode) && !S_ISDIR(base_vi->i_mode))) | |
| return -EOPNOTSUPP; | |
| if (copy_from_user(&hdr, (void __user *)arg, sizeof(hdr))) | |
| return -EFAULT; | |
| if (hdr.io_len > NTFS_STREAM_MAX_IO || | |
| (hdr.flags & ~NTFS_STREAM_FL_UTF16) || hdr.reserved) | |
| return -EINVAL; | |
| if (!hdr.name_len) | |
| return -EINVAL; | |
| utf16 = hdr.flags & NTFS_STREAM_FL_UTF16; | |
| if (utf16) { | |
| if ((hdr.name_len & 1) || | |
| hdr.name_len > NTFS_MAX_NAME_LEN * sizeof(__le16)) | |
| return -EINVAL; | |
| } else if (hdr.name_len > | |
| NTFS_MAX_NAME_LEN * NLS_MAX_CHARSET_SIZE) { | |
| return -EINVAL; | |
| } | |
| switch (op) { | |
| case NTFS_STREAM_IOCTL_READ: | |
| case NTFS_STREAM_IOCTL_WRITE: | |
| if (!hdr.io_len) | |
| return -EINVAL; | |
| if (hdr.stream_offset > S64_MAX || | |
| hdr.stream_offset > S64_MAX - hdr.io_len) | |
| return -EOVERFLOW; | |
| data_size = hdr.io_len; | |
| break; | |
| case NTFS_STREAM_IOCTL_REMOVE: | |
| if (hdr.io_len || hdr.stream_offset) | |
| return -EINVAL; | |
| data_size = 0; | |
| break; | |
| default: | |
| return -ENOTTY; | |
| } | |
| if (check_add_overflow(sizeof(hdr), (size_t)hdr.name_len, | |
| &total_size) || | |
| check_add_overflow(total_size, data_size, &total_size)) | |
| return -EOVERFLOW; | |
| if (op == NTFS_STREAM_IOCTL_READ) { | |
| req = kvmalloc(total_size, GFP_KERNEL); | |
| if (!req) | |
| return -ENOMEM; | |
| if (copy_from_user(req, (void __user *)arg, | |
| sizeof(hdr) + hdr.name_len)) { | |
| kvfree(req); | |
| return -EFAULT; | |
| } | |
| } else { | |
| req = vmemdup_user((void __user *)arg, total_size); | |
| if (IS_ERR(req)) | |
| return PTR_ERR(req); | |
| } | |
| req->bytes_returned = 0; | |
| if (req->stream_offset != hdr.stream_offset || | |
| req->io_len != hdr.io_len || req->name_len != hdr.name_len || | |
| req->flags != hdr.flags || req->reserved) { | |
| err = -EINVAL; | |
| goto out_free; | |
| } | |
| if (utf16) { | |
| sname = (__le16 *)req->buffer; | |
| sname_len = req->name_len / sizeof(__le16); | |
| } else { | |
| if (memchr(req->buffer, '\0', req->name_len)) { | |
| err = -EINVAL; | |
| goto out_free; | |
| } | |
| sname_len = ntfs_nlstoucs(base_ni->vol, req->buffer, | |
| req->name_len, &uname, NTFS_MAX_NAME_LEN); | |
| if (sname_len < 0) { | |
| err = sname_len; | |
| goto out_free; | |
| } | |
| sname = uname; | |
| } | |
| err = ntfs_check_stream_name(sname, sname_len); | |
| if (err) | |
| goto out_free; | |
| pos = hdr.stream_offset; | |
| err = ntfs_stream_ioctl_access(filp, op, pos, data_size); | |
| if (err) | |
| goto out_free; | |
| if (op == NTFS_STREAM_IOCTL_REMOVE) { | |
| err = mnt_want_write_file(filp); | |
| if (err) | |
| goto out_free; | |
| err = ntfs_remove_named_stream(base_ni, sname, sname_len, NULL); | |
| mnt_drop_write_file(filp); | |
| goto out_free; | |
| } | |
| if (op == NTFS_STREAM_IOCTL_WRITE) { | |
| err = mnt_want_write_file(filp); | |
| if (err) | |
| goto out_free; | |
| got_write = true; | |
| inode_lock(base_vi); | |
| err = file_remove_privs(filp); | |
| inode_unlock(base_vi); | |
| if (err) | |
| goto out_drop_write; | |
| if (!(base_ni->vol->vol_flags & VOLUME_IS_DIRTY)) { | |
| err = ntfs_set_volume_flags(base_ni->vol, | |
| VOLUME_IS_DIRTY); | |
| if (err) | |
| goto out_drop_write; | |
| } | |
| } | |
| err = ntfs_stream_ioctl_get_inode(base_vi, sname, sname_len, | |
| op == NTFS_STREAM_IOCTL_WRITE, &stream_vi); | |
| if (err) | |
| goto out_drop_write; | |
| err = ntfs_stream_claim(stream_vi); | |
| if (err) | |
| goto out_drop_write; | |
| claimed = true; | |
| if (op == NTFS_STREAM_IOCTL_READ) { | |
| inode_lock_shared(stream_vi); | |
| if (pos < i_size_read(stream_vi)) | |
| ret = ntfs_inode_attr_pread(stream_vi, pos, data_size, | |
| req->buffer + req->name_len); | |
| else | |
| ret = 0; | |
| inode_unlock_shared(stream_vi); | |
| if (ret < 0) | |
| err = ret; | |
| else | |
| req->bytes_returned = ret; | |
| if (!err) | |
| file_accessed(filp); | |
| } else { | |
| inode_lock(stream_vi); | |
| inode_dio_wait(stream_vi); | |
| /* Exclude faults before taking MFT record and folio locks. */ | |
| filemap_invalidate_lock(stream_vi->i_mapping); | |
| err = inode_newsize_ok(stream_vi, pos + data_size); | |
| if (!err) { | |
| ret = ntfs_inode_attr_pwrite(stream_vi, pos, data_size, | |
| req->buffer + req->name_len, false); | |
| if (ret < 0) | |
| err = ret; | |
| else | |
| req->bytes_returned = ret; | |
| } | |
| filemap_invalidate_unlock(stream_vi->i_mapping); | |
| inode_unlock(stream_vi); | |
| } | |
| if (claimed) { | |
| int put_err; | |
| put_err = ntfs_stream_put(stream_vi); | |
| claimed = false; | |
| if (!err && put_err) | |
| err = put_err; | |
| } | |
| iput(stream_vi); | |
| stream_vi = NULL; | |
| if (err) | |
| goto out_drop_write; | |
| if (op == NTFS_STREAM_IOCTL_READ) { | |
| if (copy_to_user((void __user *)arg, req, | |
| sizeof(*req) + req->name_len + | |
| req->bytes_returned)) | |
| err = -EFAULT; | |
| } else if (copy_to_user((void __user *)arg + | |
| offsetof(struct ntfs_stream, bytes_returned), | |
| &req->bytes_returned, sizeof(req->bytes_returned))) { | |
| err = -EFAULT; | |
| } | |
| goto out_drop_write; | |
| out_drop_write: | |
| if (claimed) | |
| ntfs_stream_put(stream_vi); | |
| if (stream_vi) | |
| iput(stream_vi); | |
| if (got_write) | |
| mnt_drop_write_file(filp); | |
| out_free: | |
| if (uname) | |
| kmem_cache_free(ntfs_name_cache, uname); | |
| kvfree(req); | |
| return err; | |
| } | |
| /* | |
| * ntfs_ioctl_stream_read() - Handle a named-stream read ioctl | |
| * @filp: file opened on the base file or directory | |
| * @arg: userspace pointer to the request and data buffer | |
| * | |
| * Return: 0 on success, or a negative errno. | |
| */ | |
| long ntfs_ioctl_stream_read(struct file *filp, unsigned long arg) | |
| { | |
| return ntfs_ioctl_stream(filp, arg, NTFS_STREAM_IOCTL_READ); | |
| } | |
| /* | |
| * ntfs_ioctl_stream_write() - Handle a named-stream write ioctl | |
| * @filp: file opened on the base file or directory | |
| * @arg: userspace pointer to the request and data buffer | |
| * | |
| * Create the stream if needed. | |
| * | |
| * Return: 0 on success, or a negative errno. | |
| */ | |
| long ntfs_ioctl_stream_write(struct file *filp, unsigned long arg) | |
| { | |
| return ntfs_ioctl_stream(filp, arg, NTFS_STREAM_IOCTL_WRITE); | |
| } | |
| /* | |
| * ntfs_ioctl_stream_remove() - Handle a named-stream remove ioctl | |
| * @filp: file opened on the base file or directory | |
| * @arg: userspace pointer to the request | |
| * | |
| * Return: 0 on success, or a negative errno. | |
| */ | |
| long ntfs_ioctl_stream_remove(struct file *filp, unsigned long arg) | |
| { | |
| return ntfs_ioctl_stream(filp, arg, NTFS_STREAM_IOCTL_REMOVE); | |
| } | |
| /* | |
| * ntfs_ioctl_list_streams() - List named DATA streams | |
| * @filp: file opened on the base file or directory | |
| * @arg: userspace pointer to the request and output buffer | |
| * | |
| * On -ENOSPC, return the required buffer size in the request header. | |
| * | |
| * Return: 0 on success, or a negative errno. | |
| */ | |
| int ntfs_ioctl_list_streams(struct file *filp, unsigned long arg) | |
| { | |
| struct inode *inode = file_inode(filp); | |
| struct ntfs_inode *ni = NTFS_I(inode); | |
| struct ntfs_list_streams hdr; | |
| struct ntfs_stream_entry *entry, *last_entry = NULL; | |
| size_t required = 0; | |
| void *kbuf = NULL; | |
| void __user *ubuf; | |
| struct attr_record *a; | |
| struct ntfs_attr_search_ctx *actx; | |
| int ret = 0, err, count = 0; | |
| size_t entry_size, offset = 0; | |
| const size_t name_offset = offsetof(struct ntfs_stream_entry, name); | |
| int name_len; | |
| unsigned char *sn = NULL; | |
| bool utf16; | |
| if (NVolShutdown(ni->vol)) | |
| return -EIO; | |
| if (NInoAttr(ni) || | |
| (!S_ISREG(inode->i_mode) && !S_ISDIR(inode->i_mode))) | |
| return -EOPNOTSUPP; | |
| err = inode_permission(file_mnt_idmap(filp), inode, MAY_READ); | |
| if (err) | |
| return err; | |
| if (copy_from_user(&hdr, (void __user *)arg, sizeof(hdr))) | |
| return -EFAULT; | |
| if ((hdr.flags & ~NTFS_STREAM_FL_UTF16) || hdr.reserved) | |
| return -EINVAL; | |
| utf16 = hdr.flags & NTFS_STREAM_FL_UTF16; | |
| ubuf = (void __user *)arg + sizeof(hdr); | |
| mutex_lock(&ni->mrec_lock); | |
| actx = ntfs_attr_get_search_ctx(ni, NULL); | |
| if (!actx) { | |
| mutex_unlock(&ni->mrec_lock); | |
| return -ENOMEM; | |
| } | |
| while ((err = ntfs_attrs_walk(actx)) == 0) { | |
| a = actx->attr; | |
| if (a->type != AT_DATA || !a->name_length) | |
| continue; | |
| if (a->non_resident && | |
| a->data.non_resident.lowest_vcn) | |
| continue; | |
| if (ntfs_stream_unlinked(ni, | |
| (__le16 *)((u8 *)a + | |
| le16_to_cpu(a->name_offset)), | |
| a->name_length)) | |
| continue; | |
| if (utf16) { | |
| name_len = a->name_length * sizeof(__le16); | |
| } else { | |
| name_len = ntfs_ucstonls(ni->vol, | |
| (__le16 *)((u8 *)a + | |
| le16_to_cpu(a->name_offset)), | |
| a->name_length, &sn, 0); | |
| if (name_len < 0) { | |
| if (name_len == -EILSEQ || | |
| name_len == -ENAMETOOLONG) | |
| continue; | |
| ret = name_len; | |
| goto out; | |
| } | |
| kfree(sn); | |
| sn = NULL; | |
| } | |
| entry_size = ALIGN(name_offset + name_len, 8); | |
| if (required > SIZE_MAX - entry_size) { | |
| ret = -EOVERFLOW; | |
| goto out; | |
| } | |
| required += entry_size; | |
| count++; | |
| } | |
| if (err != -ENOENT) { | |
| ret = err; | |
| goto out; | |
| } | |
| hdr.stream_count = count; | |
| hdr.bytes_returned = required; | |
| if (!count) | |
| goto out; | |
| if (hdr.buffer_size < required) { | |
| ret = -ENOSPC; | |
| goto out; | |
| } | |
| kbuf = kvzalloc(required, GFP_NOFS); | |
| if (!kbuf) { | |
| ret = -ENOMEM; | |
| goto out; | |
| } | |
| ntfs_attr_reinit_search_ctx(actx); | |
| while ((err = ntfs_attrs_walk(actx)) == 0) { | |
| a = actx->attr; | |
| if (a->type != AT_DATA || !a->name_length) | |
| continue; | |
| if (a->non_resident && | |
| a->data.non_resident.lowest_vcn) | |
| continue; | |
| if (ntfs_stream_unlinked(ni, | |
| (__le16 *)((u8 *)a + | |
| le16_to_cpu(a->name_offset)), | |
| a->name_length)) | |
| continue; | |
| if (utf16) { | |
| sn = NULL; | |
| name_len = a->name_length * sizeof(__le16); | |
| } else { | |
| name_len = ntfs_ucstonls(ni->vol, | |
| (__le16 *)((u8 *)a + | |
| le16_to_cpu(a->name_offset)), | |
| a->name_length, &sn, 0); | |
| if (name_len < 0) { | |
| if (name_len == -EILSEQ || | |
| name_len == -ENAMETOOLONG) | |
| continue; | |
| ret = name_len; | |
| goto out; | |
| } | |
| } | |
| entry_size = ALIGN(name_offset + name_len, 8); | |
| if (offset > required - entry_size) { | |
| ret = -EOVERFLOW; | |
| kfree(sn); | |
| sn = NULL; | |
| goto out; | |
| } | |
| entry = (struct ntfs_stream_entry *)(kbuf + offset); | |
| if (a->non_resident) { | |
| entry->size = le64_to_cpu(a->data.non_resident.data_size); | |
| entry->alloc_size = | |
| le64_to_cpu(a->data.non_resident.allocated_size); | |
| } else { | |
| entry->size = le32_to_cpu(a->data.resident.value_length); | |
| entry->alloc_size = le32_to_cpu(a->length) - | |
| le16_to_cpu(a->data.resident.value_offset); | |
| } | |
| entry->name_len = name_len; | |
| entry->name_offset = name_offset; | |
| if (utf16) | |
| memcpy(entry->name, | |
| (u8 *)a + le16_to_cpu(a->name_offset), name_len); | |
| else | |
| memcpy(entry->name, sn, name_len); | |
| kfree(sn); | |
| sn = NULL; | |
| entry->next_entry_off = entry_size; | |
| last_entry = entry; | |
| offset += entry_size; | |
| } | |
| if (err != -ENOENT) { | |
| ret = err; | |
| } else { | |
| /* The chain terminates at the last entry. */ | |
| if (last_entry) | |
| last_entry->next_entry_off = 0; | |
| hdr.bytes_returned = offset; | |
| } | |
| out: | |
| kfree(sn); | |
| ntfs_attr_put_search_ctx(actx); | |
| mutex_unlock(&ni->mrec_lock); | |
| if (ret && ret != -ENOSPC) | |
| goto out_free; | |
| if (!ret && kbuf && copy_to_user(ubuf, kbuf, hdr.bytes_returned)) { | |
| ret = -EFAULT; | |
| goto out_free; | |
| } | |
| if (copy_to_user((void __user *)arg, &hdr, sizeof(hdr))) | |
| ret = -EFAULT; | |
| out_free: | |
| kvfree(kbuf); | |
| return ret; | |
| } | |
| const struct file_operations ntfs_stream_file_ops = { | |
| .llseek = ntfs_file_llseek, | |
| .read_iter = ntfs_file_read_iter, | |
| .write_iter = ntfs_file_write_iter, | |
| .fsync = ntfs_file_fsync, | |
| .fallocate = ntfs_fallocate, | |
| .mmap_prepare = ntfs_file_mmap_prepare, | |
| .open = ntfs_stream_file_open, | |
| .release = ntfs_stream_file_release, | |
| .splice_read = ntfs_file_splice_read, | |
| .splice_write = iter_file_splice_write, | |
| }; | |
| const struct inode_operations ntfs_stream_inode_ops = { | |
| .permission = ntfs_stream_permission, | |
| .setattr = ntfs_stream_setattr, | |
| .getattr = ntfs_stream_getattr, | |
| .listxattr = ntfs_stream_listxattr, | |
| #ifdef CONFIG_NTFS_FS_POSIX_ACL | |
| .get_acl = ntfs_stream_get_acl, | |
| .set_acl = ntfs_stream_set_acl, | |
| #endif | |
| .update_time = ntfs_stream_update_time, | |
| .fiemap = ntfs_fiemap, | |
| }; |