mirror of
git://git.kernel.org/pub/scm/linux/kernel/git/xiang/erofs.git
synced 2026-10-28 12:53:14 +01:00
An ADS (alternative data stream) is a named $DATA (0x80) attribute.
Until now ntfs3 ignored named data attributes entirely, so the only
stream reachable from userspace was the unnamed one.
Introduce a colon-delimited name syntax so streams can be reached
through the regular VFS interfaces. ntfs_nls_to_utf16() now splits a
lookup name at ':' and stores the stream part in cpu_str::ads_len (the
previously unused padding byte). ntfs_iget5_flags() first instantiates
the base inode, then allocates a second inode whose ->base points at it
and whose ->file.ads holds the stream name. ntfs_test_inode() compares
the stream name as well as the MFT reference, so base and streams get
distinct inodes for the same record.
Because the MFT record belongs to the base inode, the many helpers in
frecord.c and attrlist.c that operate on the record redirect to
ni->base, and ni_lock() and friends take the base inode's mutex. The
attrib.c paths that used to hardcode an unnamed $DATA lookup now pass
ni->file.ads.{name,len}. ni_write_inode() and ni_write_parents() are
no-ops for stream inodes, and ntfs_setattr() drops ATTR_SIZE for them.
Usage, for a file with streams 'ads1' and 'ads2':
cat file:query_streams - list stream names, one per line
cat file:ads1 - read a stream
touch file:ads3 - create a stream on an existing file
rm file:ads1 - remove a stream
The pseudo-stream 'query_streams' is handled in ntfs_file_read_iter()
via ni_query_ads(), which enumerates named $DATA attributes and returns
their names separated by '\n'.
The feature is controlled by the new 'ads' mount option, enabled by
default; mount with 'ads=0' to restore the previous behaviour.
Not implemented yet:
- creating a file and a stream in a single call
- renaming (moving) a stream
Signed-off-by: Konstantin Komarov <[email protected]>
440 lines
8.7 KiB
C
440 lines
8.7 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
*
|
|
* Copyright (C) 2019-2021 Paragon Software GmbH, All rights reserved.
|
|
*
|
|
*/
|
|
|
|
#include <linux/fs.h>
|
|
|
|
#include "debug.h"
|
|
#include "ntfs.h"
|
|
#include "ntfs_fs.h"
|
|
|
|
/*
|
|
* al_is_valid_le
|
|
*
|
|
* Return: True if @le is valid.
|
|
*/
|
|
static inline bool al_is_valid_le(const struct ntfs_inode *ni,
|
|
struct ATTR_LIST_ENTRY *le)
|
|
{
|
|
ni = ni->base;
|
|
if (!le || !ni->attr_list.le || !ni->attr_list.size)
|
|
return false;
|
|
|
|
return PtrOffset(ni->attr_list.le, le) + le16_to_cpu(le->size) <=
|
|
ni->attr_list.size;
|
|
}
|
|
|
|
void al_destroy(struct ntfs_inode *ni)
|
|
{
|
|
ni = ni->base;
|
|
run_close(&ni->attr_list.run);
|
|
kvfree(ni->attr_list.le);
|
|
ni->attr_list.le = NULL;
|
|
ni->attr_list.size = 0;
|
|
ni->attr_list.dirty = false;
|
|
}
|
|
|
|
/*
|
|
* ntfs_load_attr_list
|
|
*
|
|
* This method makes sure that the ATTRIB list, if present,
|
|
* has been properly set up.
|
|
*/
|
|
int ntfs_load_attr_list(struct ntfs_inode *ni, struct ATTRIB *attr)
|
|
{
|
|
int err;
|
|
size_t lsize;
|
|
void *le = NULL;
|
|
|
|
ni = ni->base;
|
|
if (ni->attr_list.size)
|
|
return 0;
|
|
|
|
if (!attr->non_res) {
|
|
lsize = le32_to_cpu(attr->res.data_size);
|
|
if (!lsize) {
|
|
err = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
/* attr is resident: lsize < record_size (1K or 4K) */
|
|
le = kvmalloc(al_aligned(lsize), GFP_KERNEL);
|
|
if (!le) {
|
|
err = -ENOMEM;
|
|
goto out;
|
|
}
|
|
memcpy(le, resident_data(attr), lsize);
|
|
} else if (attr->nres.svcn) {
|
|
err = -EINVAL;
|
|
goto out;
|
|
} else {
|
|
u16 run_off = le16_to_cpu(attr->nres.run_off);
|
|
|
|
lsize = le64_to_cpu(attr->nres.data_size);
|
|
if (!lsize) {
|
|
err = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
run_init(&ni->attr_list.run);
|
|
|
|
if (run_off > le32_to_cpu(attr->size)) {
|
|
err = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
err = run_unpack_ex(&ni->attr_list.run, ni->mi.sbi, ni->mi.rno,
|
|
0, le64_to_cpu(attr->nres.evcn), 0,
|
|
Add2Ptr(attr, run_off),
|
|
le32_to_cpu(attr->size) - run_off);
|
|
if (err < 0)
|
|
goto out;
|
|
|
|
/* attr is nonresident.
|
|
* The worst case:
|
|
* 1T (2^40) extremely fragmented file.
|
|
* cluster = 4K (2^12) => 2^28 fragments
|
|
* 2^9 fragments per one record => 2^19 records
|
|
* 2^5 bytes of ATTR_LIST_ENTRY per one record => 2^24 bytes.
|
|
*
|
|
* the result is 16M bytes per attribute list.
|
|
* Use kvmalloc to allocate in range [several Kbytes - dozen Mbytes]
|
|
*/
|
|
le = kvmalloc(al_aligned(lsize), GFP_KERNEL);
|
|
if (!le) {
|
|
err = -ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
err = ntfs_read_run_nb(ni->mi.sbi, &ni->attr_list.run, 0, le,
|
|
lsize, NULL);
|
|
if (err)
|
|
goto out;
|
|
}
|
|
|
|
ni->attr_list.size = lsize;
|
|
ni->attr_list.le = le;
|
|
|
|
return 0;
|
|
|
|
out:
|
|
ni->attr_list.le = le;
|
|
al_destroy(ni);
|
|
|
|
return err;
|
|
}
|
|
|
|
/*
|
|
* al_enumerate
|
|
*
|
|
* Return:
|
|
* * The next list le.
|
|
* * If @le is NULL then return the first le.
|
|
*/
|
|
struct ATTR_LIST_ENTRY *al_enumerate(struct ntfs_inode *ni,
|
|
struct ATTR_LIST_ENTRY *le)
|
|
{
|
|
size_t off;
|
|
u16 sz;
|
|
const unsigned le_min_size = le_size(0);
|
|
|
|
if (!le) {
|
|
le = ni->attr_list.le;
|
|
} else {
|
|
sz = le16_to_cpu(le->size);
|
|
if (sz < le_min_size) {
|
|
/* Impossible 'cause we should not return such le. */
|
|
return NULL;
|
|
}
|
|
le = Add2Ptr(le, sz);
|
|
}
|
|
|
|
/* Check boundary. */
|
|
off = PtrOffset(ni->attr_list.le, le);
|
|
if (off + le_min_size > ni->attr_list.size) {
|
|
/* The regular end of list. */
|
|
return NULL;
|
|
}
|
|
|
|
sz = le16_to_cpu(le->size);
|
|
|
|
/* Check le for errors. */
|
|
if (sz < le_min_size || off + sz > ni->attr_list.size ||
|
|
sz < le->name_off + le->name_len * sizeof(short)) {
|
|
return NULL;
|
|
}
|
|
|
|
return le;
|
|
}
|
|
|
|
/*
|
|
* al_find_le
|
|
*
|
|
* Find the first le in the list which matches type, name and VCN.
|
|
*
|
|
* Return: NULL if not found.
|
|
*/
|
|
struct ATTR_LIST_ENTRY *al_find_le(struct ntfs_inode *ni,
|
|
struct ATTR_LIST_ENTRY *le,
|
|
const struct ATTRIB *attr)
|
|
{
|
|
CLST svcn = attr_svcn(attr);
|
|
|
|
return al_find_ex(ni, le, attr->type, attr_name(attr), attr->name_len,
|
|
&svcn);
|
|
}
|
|
|
|
/*
|
|
* al_find_ex
|
|
*
|
|
* Find the first le in the list which matches type, name and VCN.
|
|
*
|
|
* Return: NULL if not found.
|
|
*/
|
|
struct ATTR_LIST_ENTRY *al_find_ex(struct ntfs_inode *ni,
|
|
struct ATTR_LIST_ENTRY *le,
|
|
enum ATTR_TYPE type, const __le16 *name,
|
|
u8 name_len, const CLST *vcn)
|
|
{
|
|
struct ATTR_LIST_ENTRY *ret = NULL;
|
|
u32 type_in = le32_to_cpu(type);
|
|
|
|
ni = ni->base;
|
|
while ((le = al_enumerate(ni, le))) {
|
|
u64 le_vcn;
|
|
int diff = le32_to_cpu(le->type) - type_in;
|
|
|
|
/* List entries are sorted by type, name and VCN. */
|
|
if (diff < 0)
|
|
continue;
|
|
|
|
if (diff > 0)
|
|
return ret;
|
|
|
|
if (le->name_len != name_len)
|
|
continue;
|
|
|
|
le_vcn = le64_to_cpu(le->vcn);
|
|
if (!le_vcn) {
|
|
/*
|
|
* Compare entry names only for entry with vcn == 0.
|
|
*/
|
|
diff = ntfs_cmp_names(le_name(le), name_len, name,
|
|
name_len, ni->mi.sbi->upcase,
|
|
true);
|
|
if (diff < 0)
|
|
continue;
|
|
|
|
if (diff > 0)
|
|
return ret;
|
|
}
|
|
|
|
if (!vcn)
|
|
return le;
|
|
|
|
if (*vcn == le_vcn)
|
|
return le;
|
|
|
|
if (*vcn < le_vcn)
|
|
return ret;
|
|
|
|
ret = le;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* al_find_le_to_insert
|
|
*
|
|
* Find the first list entry which matches type, name and VCN.
|
|
*/
|
|
static struct ATTR_LIST_ENTRY *al_find_le_to_insert(struct ntfs_inode *ni,
|
|
enum ATTR_TYPE type,
|
|
const __le16 *name,
|
|
u8 name_len, CLST vcn)
|
|
{
|
|
struct ATTR_LIST_ENTRY *le = NULL, *prev;
|
|
u32 type_in = le32_to_cpu(type);
|
|
|
|
ni = ni->base;
|
|
/* List entries are sorted by type, name and VCN. */
|
|
while ((le = al_enumerate(ni, prev = le))) {
|
|
int diff = le32_to_cpu(le->type) - type_in;
|
|
|
|
if (diff < 0)
|
|
continue;
|
|
|
|
if (diff > 0)
|
|
return le;
|
|
|
|
if (!le->vcn) {
|
|
/*
|
|
* Compare entry names only for entry with vcn == 0.
|
|
*/
|
|
diff = ntfs_cmp_names(le_name(le), le->name_len, name,
|
|
name_len, ni->mi.sbi->upcase,
|
|
true);
|
|
if (diff < 0)
|
|
continue;
|
|
|
|
if (diff > 0)
|
|
return le;
|
|
}
|
|
|
|
if (le64_to_cpu(le->vcn) >= vcn)
|
|
return le;
|
|
}
|
|
|
|
return prev ? Add2Ptr(prev, le16_to_cpu(prev->size)) : ni->attr_list.le;
|
|
}
|
|
|
|
/*
|
|
* al_add_le
|
|
*
|
|
* Add an "attribute list entry" to the list.
|
|
*/
|
|
int al_add_le(struct ntfs_inode *ni, enum ATTR_TYPE type, const __le16 *name,
|
|
u8 name_len, CLST svcn, __le16 id, const struct MFT_REF *ref,
|
|
struct ATTR_LIST_ENTRY **new_le)
|
|
{
|
|
int err;
|
|
struct ATTRIB *attr;
|
|
struct ATTR_LIST_ENTRY *le;
|
|
size_t off;
|
|
u16 sz;
|
|
size_t asize, new_asize, old_size;
|
|
u64 new_size;
|
|
typeof(ni->attr_list) *al = &ni->attr_list;
|
|
|
|
ni = ni->base;
|
|
/*
|
|
* Compute the size of the new 'le'
|
|
*/
|
|
sz = le_size(name_len);
|
|
old_size = al->size;
|
|
new_size = old_size + sz;
|
|
asize = al_aligned(old_size);
|
|
new_asize = al_aligned(new_size);
|
|
|
|
/* Scan forward to the point at which the new 'le' should be inserted. */
|
|
le = al_find_le_to_insert(ni, type, name, name_len, svcn);
|
|
off = PtrOffset(al->le, le);
|
|
|
|
if (new_size > asize) {
|
|
void *ptr = kmalloc(new_asize, GFP_NOFS);
|
|
|
|
if (!ptr)
|
|
return -ENOMEM;
|
|
|
|
memcpy(ptr, al->le, off);
|
|
memcpy(Add2Ptr(ptr, off + sz), le, old_size - off);
|
|
le = Add2Ptr(ptr, off);
|
|
kvfree(al->le);
|
|
al->le = ptr;
|
|
} else {
|
|
memmove(Add2Ptr(le, sz), le, old_size - off);
|
|
}
|
|
*new_le = le;
|
|
|
|
al->size = new_size;
|
|
|
|
le->type = type;
|
|
le->size = cpu_to_le16(sz);
|
|
le->name_len = name_len;
|
|
le->name_off = offsetof(struct ATTR_LIST_ENTRY, name);
|
|
le->vcn = cpu_to_le64(svcn);
|
|
le->ref = *ref;
|
|
le->id = id;
|
|
memcpy(le->name, name, sizeof(short) * name_len);
|
|
|
|
err = attr_set_size_ex(ni, ATTR_LIST, NULL, 0, &al->run, new_size,
|
|
&new_size, true, &attr, false);
|
|
if (err) {
|
|
/* Undo memmove above. */
|
|
memmove(le, Add2Ptr(le, sz), old_size - off);
|
|
al->size = old_size;
|
|
return err;
|
|
}
|
|
|
|
al->dirty = true;
|
|
|
|
if (attr && attr->non_res) {
|
|
err = ntfs_sb_write_run(ni->mi.sbi, &al->run, 0, al->le,
|
|
al->size, 0);
|
|
if (err)
|
|
return err;
|
|
al->dirty = false;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* al_remove_le - Remove @le from attribute list.
|
|
*/
|
|
bool al_remove_le(struct ntfs_inode *ni, struct ATTR_LIST_ENTRY *le)
|
|
{
|
|
u16 size;
|
|
size_t off;
|
|
typeof(ni->attr_list) *al;
|
|
|
|
ni = ni->base;
|
|
al = &ni->attr_list;
|
|
if (!al_is_valid_le(ni, le))
|
|
return false;
|
|
|
|
/* Save on stack the size of 'le' */
|
|
size = le16_to_cpu(le->size);
|
|
off = PtrOffset(al->le, le);
|
|
|
|
memmove(le, Add2Ptr(le, size), al->size - (off + size));
|
|
|
|
al->size -= size;
|
|
al->dirty = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
int al_update(struct ntfs_inode *ni, int sync)
|
|
{
|
|
int err;
|
|
struct ATTRIB *attr;
|
|
typeof(ni->attr_list) *al;
|
|
|
|
ni = ni->base;
|
|
al = &ni->attr_list;
|
|
|
|
if (!al->dirty || !al->size)
|
|
return 0;
|
|
|
|
/*
|
|
* Attribute list increased on demand in al_add_le.
|
|
* Attribute list decreased here.
|
|
*/
|
|
err = attr_set_size_ex(ni, ATTR_LIST, NULL, 0, &al->run, al->size, NULL,
|
|
false, &attr, false);
|
|
if (err)
|
|
goto out;
|
|
|
|
if (!attr->non_res) {
|
|
memcpy(resident_data(attr), al->le, al->size);
|
|
} else {
|
|
err = ntfs_sb_write_run(ni->mi.sbi, &al->run, 0, al->le,
|
|
al->size, sync);
|
|
if (err)
|
|
goto out;
|
|
|
|
attr->nres.valid_size = attr->nres.data_size;
|
|
}
|
|
|
|
ni->mi.dirty = true;
|
|
al->dirty = false;
|
|
|
|
out:
|
|
return err;
|
|
}
|