Files
Aaru/Aaru.Filesystems/NTFS/Xattrs.cs

588 lines
24 KiB
C#

// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Xattrs.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Microsoft NT File System plugin.
//
// --[ License ] --------------------------------------------------------------
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using Aaru.CommonTypes.Enums;
using Aaru.Helpers;
using Aaru.Logging;
namespace Aaru.Filesystems;
/// <inheritdoc />
public sealed partial class NTFS
{
const string NT_ACL = "com.microsoft.ntacl";
const string EA_LXUID = "$LXUID";
const string EA_LXGID = "$LXGID";
const string EA_LXMOD = "$LXMOD";
const string EA_LXDEV = "$LXDEV";
const string EA_LXATTRB = "$LXATTRB";
/// <inheritdoc />
public ErrorNumber ListXAttr(string path, out List<string> xattrs)
{
xattrs = null;
if(!_mounted) return ErrorNumber.AccessDenied;
// Normalize path
string normalizedPath = string.IsNullOrWhiteSpace(path) ? "/" : path;
if(!normalizedPath.StartsWith("/", StringComparison.Ordinal)) normalizedPath = "/" + normalizedPath;
uint mftRecordNumber;
if(normalizedPath == "/")
mftRecordNumber = (uint)SystemFileNumber.Root;
else
{
ErrorNumber resolveErrno = ResolvePathToMftRecord(normalizedPath, out mftRecordNumber);
if(resolveErrno != ErrorNumber.NoError) return resolveErrno;
}
xattrs = [];
var foundInlineSd = false;
uint securityId = 0;
// Find all attributes across base + extension records
ErrorNumber findErrno = FindAllAttributes(mftRecordNumber, out List<FoundAttribute> attrs);
if(findErrno != ErrorNumber.NoError) return findErrno;
foreach(FoundAttribute attr in attrs)
{
var attrType = (AttributeType)BitConverter.ToUInt32(attr.RecordData, attr.Offset);
byte nonResident = attr.RecordData[attr.Offset + 8];
switch(attrType)
{
// Security descriptor → expose as "com.microsoft.ntacl"
case AttributeType.SecurityDescriptor when nonResident == 0:
{
var valueLength = BitConverter.ToUInt32(attr.RecordData, attr.Offset + 0x10);
if(valueLength > 0)
{
foundInlineSd = true;
xattrs.Add(NT_ACL);
}
break;
}
// Track security_id from $STANDARD_INFORMATION for $Secure lookup
case AttributeType.StandardInformation when nonResident == 0:
{
var valueOffset = BitConverter.ToUInt16(attr.RecordData, attr.Offset + 0x14);
var valueLength = BitConverter.ToUInt32(attr.RecordData, attr.Offset + 0x10);
int valueStart = attr.Offset + valueOffset;
if(valueLength >= (uint)Marshal.SizeOf<StandardInformationV3>() &&
valueStart + Marshal.SizeOf<StandardInformationV3>() <= attr.RecordData.Length)
{
StandardInformationV3 stdInfo =
Marshal.ByteArrayToStructureLittleEndian<StandardInformationV3>(attr.RecordData,
valueStart,
Marshal.SizeOf<StandardInformationV3>());
securityId = stdInfo.security_id;
}
break;
}
// Extended Attributes → expose each EA by its raw name
case AttributeType.Ea:
{
ErrorNumber eaErrno = ReadEaAttributeData(attr.RecordData,
attr.Offset,
nonResident,
out byte[] eaData,
out int eaLength);
if(eaErrno == ErrorNumber.NoError && eaLength > 0) EnumerateEas(eaData, 0, eaLength, xattrs);
break;
}
// Named $DATA attributes → Alternate Data Streams (no prefix)
case AttributeType.Data:
{
byte nameLength = attr.RecordData[attr.Offset + 9];
if(nameLength > 0)
{
var nameOffset = BitConverter.ToUInt16(attr.RecordData, attr.Offset + 0x0A);
string streamName =
Encoding.Unicode.GetString(attr.RecordData, attr.Offset + nameOffset, nameLength * 2);
xattrs.Add(streamName);
}
break;
}
}
}
// If no inline $SECURITY_DESCRIPTOR was found, check $Secure by security_id
if(!foundInlineSd && securityId != 0 && _securityDescriptors.ContainsKey(securityId)) xattrs.Add(NT_ACL);
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber GetXattr(string path, string xattr, ref byte[] buf)
{
if(!_mounted) return ErrorNumber.AccessDenied;
if(string.IsNullOrWhiteSpace(xattr)) return ErrorNumber.InvalidArgument;
// Normalize path
string normalizedPath = string.IsNullOrWhiteSpace(path) ? "/" : path;
if(!normalizedPath.StartsWith("/", StringComparison.Ordinal)) normalizedPath = "/" + normalizedPath;
uint mftRecordNumber;
if(normalizedPath == "/")
mftRecordNumber = (uint)SystemFileNumber.Root;
else
{
ErrorNumber resolveErrno = ResolvePathToMftRecord(normalizedPath, out mftRecordNumber);
if(resolveErrno != ErrorNumber.NoError) return resolveErrno;
}
ErrorNumber errno = ReadMftRecord(mftRecordNumber, out byte[] recordData);
if(errno != ErrorNumber.NoError) return errno;
MftRecord header = ParseMftRecordHeader(recordData);
if(header.magic != NtfsRecordMagic.File)
{
AaruLogging.Debug(MODULE_NAME, "MFT record {0} has invalid magic", mftRecordNumber);
return ErrorNumber.InvalidArgument;
}
if(!header.flags.HasFlag(MftRecordFlags.InUse))
{
AaruLogging.Debug(MODULE_NAME, "MFT record {0} is not in use", mftRecordNumber);
return ErrorNumber.NoSuchFile;
}
// Determine what kind of xattr is being requested
if(xattr == NT_ACL) return ReadSecurityDescriptor(recordData, header, mftRecordNumber, ref buf);
// WSL metadata EAs are not exposed as xattrs
if(xattr is EA_LXUID or EA_LXGID or EA_LXMOD or EA_LXDEV or EA_LXATTRB)
return ErrorNumber.NoSuchExtendedAttribute;
// Try reading as an Extended Attribute first
ErrorNumber eaErrno = ReadEa(recordData, header, mftRecordNumber, xattr, ref buf);
if(eaErrno != ErrorNumber.NoSuchExtendedAttribute) return eaErrno;
// Otherwise it's an Alternate Data Stream name
return ReadAlternateDataStream(recordData, header, mftRecordNumber, xattr, ref buf);
}
/// <summary>
/// Reads the security descriptor for a file, first trying the inline $SECURITY_DESCRIPTOR attribute,
/// then falling back to $Secure via the security_id in $STANDARD_INFORMATION.
/// </summary>
/// <param name="recordData">Raw MFT record data.</param>
/// <param name="header">Parsed MFT record header.</param>
/// <param name="mftRecordNumber">MFT record number (for attribute list traversal).</param>
/// <param name="buf">Output buffer for the raw security descriptor bytes.</param>
/// <returns>Error number indicating success or failure.</returns>
ErrorNumber ReadSecurityDescriptor(byte[] recordData, in MftRecord header, uint mftRecordNumber, ref byte[] buf)
{
// Try inline $SECURITY_DESCRIPTOR attribute first
ErrorNumber findErrno = FindAttributes(recordData,
header,
mftRecordNumber,
AttributeType.SecurityDescriptor,
null,
out List<FoundAttribute> results);
if(findErrno == ErrorNumber.NoError && results.Count > 0)
{
FoundAttribute attr = results[0];
byte nonResident = attr.RecordData[attr.Offset + 8];
if(nonResident == 0)
{
var valueOffset = BitConverter.ToUInt16(attr.RecordData, attr.Offset + 0x14);
var valueLength = BitConverter.ToUInt32(attr.RecordData, attr.Offset + 0x10);
int valueStart = attr.Offset + valueOffset;
if(valueStart + valueLength <= attr.RecordData.Length && valueLength > 0)
{
buf = new byte[valueLength];
Array.Copy(attr.RecordData, valueStart, buf, 0, valueLength);
return ErrorNumber.NoError;
}
}
}
// Fall back to $Secure via security_id from $STANDARD_INFORMATION
findErrno = FindAttributes(recordData,
header,
mftRecordNumber,
AttributeType.StandardInformation,
null,
out List<FoundAttribute> stdInfoResults);
if(findErrno != ErrorNumber.NoError || stdInfoResults.Count == 0) return ErrorNumber.NoSuchExtendedAttribute;
FoundAttribute stdAttr = stdInfoResults[0];
byte stdNonRes = stdAttr.RecordData[stdAttr.Offset + 8];
if(stdNonRes != 0) return ErrorNumber.NoSuchExtendedAttribute;
var stdValueOffset = BitConverter.ToUInt16(stdAttr.RecordData, stdAttr.Offset + 0x14);
var stdValueLength = BitConverter.ToUInt32(stdAttr.RecordData, stdAttr.Offset + 0x10);
int stdValueStart = stdAttr.Offset + stdValueOffset;
if(stdValueLength < (uint)Marshal.SizeOf<StandardInformationV3>() ||
stdValueStart + Marshal.SizeOf<StandardInformationV3>() > stdAttr.RecordData.Length)
return ErrorNumber.NoSuchExtendedAttribute;
StandardInformationV3 stdInfo =
Marshal.ByteArrayToStructureLittleEndian<StandardInformationV3>(stdAttr.RecordData,
stdValueStart,
Marshal.SizeOf<StandardInformationV3>());
if(stdInfo.security_id == 0 || !_securityDescriptors.TryGetValue(stdInfo.security_id, out byte[] sd))
return ErrorNumber.NoSuchExtendedAttribute;
buf = new byte[sd.Length];
Array.Copy(sd, 0, buf, 0, sd.Length);
return ErrorNumber.NoError;
}
/// <summary>Reads a specific Extended Attribute value from the $EA attribute in an MFT record.</summary>
/// <param name="recordData">Raw MFT record data.</param>
/// <param name="header">Parsed MFT record header.</param>
/// <param name="mftRecordNumber">MFT record number (for attribute list traversal).</param>
/// <param name="eaName">EA name to search for.</param>
/// <param name="buf">Output buffer for the EA value bytes.</param>
/// <returns>Error number indicating success or failure.</returns>
ErrorNumber ReadEa(byte[] recordData, in MftRecord header, uint mftRecordNumber, string eaName, ref byte[] buf)
{
ErrorNumber findErrno = FindAttributes(recordData,
header,
mftRecordNumber,
AttributeType.Ea,
null,
out List<FoundAttribute> results);
if(findErrno != ErrorNumber.NoError || results.Count == 0) return ErrorNumber.NoSuchExtendedAttribute;
FoundAttribute attr = results[0];
byte nonResident = attr.RecordData[attr.Offset + 8];
ErrorNumber eaErrno =
ReadEaAttributeData(attr.RecordData, attr.Offset, nonResident, out byte[] eaData, out int eaLength);
if(eaErrno != ErrorNumber.NoError) return eaErrno;
if(eaLength > 0) return FindEaByName(eaData, 0, eaLength, eaName, ref buf);
return ErrorNumber.NoSuchExtendedAttribute;
}
/// <summary>Reads a named Alternate Data Stream from an MFT record.</summary>
/// <param name="recordData">Raw MFT record data.</param>
/// <param name="header">Parsed MFT record header.</param>
/// <param name="mftRecordNumber">MFT record number (for attribute list traversal).</param>
/// <param name="streamName">ADS name to search for.</param>
/// <param name="buf">Output buffer for the stream data.</param>
/// <returns>Error number indicating success or failure.</returns>
ErrorNumber ReadAlternateDataStream(byte[] recordData, in MftRecord header, uint mftRecordNumber, string streamName,
ref byte[] buf)
{
ErrorNumber findErrno = FindAttributes(recordData,
header,
mftRecordNumber,
AttributeType.Data,
streamName,
out List<FoundAttribute> results);
if(findErrno != ErrorNumber.NoError || results.Count == 0) return ErrorNumber.NoSuchExtendedAttribute;
FoundAttribute attr = results[0];
byte nonResident = attr.RecordData[attr.Offset + 8];
if(nonResident == 0)
{
// Resident ADS
var valueOffset = BitConverter.ToUInt16(attr.RecordData, attr.Offset + 0x14);
var valueLength = BitConverter.ToUInt32(attr.RecordData, attr.Offset + 0x10);
int valueStart = attr.Offset + valueOffset;
if(valueStart + valueLength > attr.RecordData.Length) return ErrorNumber.NoSuchExtendedAttribute;
buf = new byte[valueLength];
Array.Copy(attr.RecordData, valueStart, buf, 0, valueLength);
return ErrorNumber.NoError;
}
// Non-resident ADS — assemble data runs from all extents
ErrorNumber asmErrno = AssembleNonResidentRuns(mftRecordNumber,
AttributeType.Data,
streamName,
out List<(long offset, long length)> dataRuns,
out long dataSize,
out _,
out _,
out _);
if(asmErrno != ErrorNumber.NoError) return asmErrno;
return ReadNonResidentData(dataRuns, dataSize, ref buf);
}
/// <summary>Enumerates EA entries and adds their names to the xattr list.</summary>
/// <param name="data">Buffer containing the EA data.</param>
/// <param name="start">Start offset of the EA data in the buffer.</param>
/// <param name="length">Total length of the EA data.</param>
/// <param name="xattrs">List to add xattr names to.</param>
static void EnumerateEas(byte[] data, int start, int length, List<string> xattrs)
{
int pos = start;
int end = start + length;
while(pos + Marshal.SizeOf<EaAttribute>() <= end)
{
EaAttribute ea =
Marshal.ByteArrayToStructureLittleEndian<EaAttribute>(data, pos, Marshal.SizeOf<EaAttribute>());
int nameStart = pos + Marshal.SizeOf<EaAttribute>();
if(nameStart + ea.ea_name_length > end) break;
string eaName = Encoding.ASCII.GetString(data, nameStart, ea.ea_name_length);
// Skip WSL metadata EAs — they are parsed into Stat fields instead
if(eaName is EA_LXUID or EA_LXGID or EA_LXMOD or EA_LXDEV or EA_LXATTRB)
{
if(ea.next_entry_offset == 0) break;
pos += (int)ea.next_entry_offset;
continue;
}
xattrs.Add(eaName);
if(ea.next_entry_offset == 0) break;
pos += (int)ea.next_entry_offset;
}
}
/// <summary>Finds an EA by name and returns its value.</summary>
/// <param name="data">Buffer containing the EA data.</param>
/// <param name="start">Start offset of the EA data in the buffer.</param>
/// <param name="length">Total length of the EA data.</param>
/// <param name="eaName">EA name to search for.</param>
/// <param name="buf">Output buffer for the EA value bytes.</param>
/// <returns>Error number indicating success or failure.</returns>
static ErrorNumber FindEaByName(byte[] data, int start, int length, string eaName, ref byte[] buf)
{
int pos = start;
int end = start + length;
while(pos + Marshal.SizeOf<EaAttribute>() <= end)
{
EaAttribute ea =
Marshal.ByteArrayToStructureLittleEndian<EaAttribute>(data, pos, Marshal.SizeOf<EaAttribute>());
int nameStart = pos + Marshal.SizeOf<EaAttribute>();
if(nameStart + ea.ea_name_length > end) break;
string name = Encoding.ASCII.GetString(data, nameStart, ea.ea_name_length);
if(string.Equals(name, eaName, StringComparison.OrdinalIgnoreCase))
{
// EA value follows the name + NUL terminator
int valueStart = nameStart + ea.ea_name_length + 1;
if(valueStart + ea.ea_value_length > end) return ErrorNumber.InvalidArgument;
buf = new byte[ea.ea_value_length];
Array.Copy(data, valueStart, buf, 0, ea.ea_value_length);
return ErrorNumber.NoError;
}
if(ea.next_entry_offset == 0) break;
pos += (int)ea.next_entry_offset;
}
return ErrorNumber.NoSuchExtendedAttribute;
}
/// <summary>Reads EA attribute data from either resident or non-resident storage.</summary>
/// <param name="recordData">Raw MFT record data.</param>
/// <param name="attrOffset">Offset of the EA attribute header within the record.</param>
/// <param name="nonResident">Non-resident flag (0 = resident).</param>
/// <param name="eaData">Output buffer containing the raw EA data.</param>
/// <param name="eaLength">Output length of valid EA data.</param>
/// <returns>Error number indicating success or failure.</returns>
ErrorNumber ReadEaAttributeData(byte[] recordData, int attrOffset, byte nonResident, out byte[] eaData,
out int eaLength)
{
eaData = null;
eaLength = 0;
if(nonResident == 0)
{
// Resident EA
var valueOffset = BitConverter.ToUInt16(recordData, attrOffset + 0x14);
var valueLength = BitConverter.ToUInt32(recordData, attrOffset + 0x10);
int valueStart = attrOffset + valueOffset;
if(valueStart + valueLength > recordData.Length) return ErrorNumber.InvalidArgument;
eaData = recordData;
eaLength = (int)valueLength;
// Adjust: EnumerateEas/FindEaByName/ParseWslEas expect start=0 for the EA data buffer
// but here eaData is the full record, so we need to slice it
var slice = new byte[valueLength];
Array.Copy(recordData, valueStart, slice, 0, valueLength);
eaData = slice;
eaLength = (int)valueLength;
return ErrorNumber.NoError;
}
// Non-resident EA — read via data runs
var attrLength = BitConverter.ToUInt32(recordData, attrOffset + 4);
NonResidentAttributeRecord nrAttr =
Marshal.ByteArrayToStructureLittleEndian<NonResidentAttributeRecord>(recordData,
attrOffset,
Marshal.SizeOf<NonResidentAttributeRecord>());
int runListOffset = attrOffset + nrAttr.mapping_pairs_offset;
List<(long offset, long length)> dataRuns =
ParseDataRuns(recordData, runListOffset, attrOffset + (int)attrLength);
byte[] buf = Array.Empty<byte>();
ErrorNumber errno = ReadNonResidentData(dataRuns, (long)nrAttr.data_size, ref buf);
if(errno != ErrorNumber.NoError) return errno;
eaData = buf;
eaLength = (int)nrAttr.data_size;
return ErrorNumber.NoError;
}
/// <summary>Reads non-resident data from disk using parsed data runs.</summary>
/// <param name="dataRuns">List of (absolute cluster offset, length in clusters) tuples.</param>
/// <param name="dataSize">Logical data size in bytes.</param>
/// <param name="buf">Output buffer for the data.</param>
/// <returns>Error number indicating success or failure.</returns>
ErrorNumber ReadNonResidentData(List<(long offset, long length)> dataRuns, long dataSize, ref byte[] buf)
{
if(dataSize > int.MaxValue)
{
AaruLogging.Debug(MODULE_NAME, "Non-resident data size {0} exceeds maximum array size", dataSize);
return ErrorNumber.OutOfRange;
}
buf = new byte[dataSize];
long bytesRead = 0;
foreach((long clusterOffset, long clusterLength) in dataRuns)
{
long runBytes = clusterLength * _bytesPerCluster;
// Sparse run
if(clusterOffset == 0)
{
long toClear = Math.Min(runBytes, dataSize - bytesRead);
Array.Clear(buf, (int)bytesRead, (int)toClear);
bytesRead += toClear;
continue;
}
for(long c = 0; c < clusterLength && bytesRead < dataSize; c++)
{
ulong sectorStart = (ulong)(clusterOffset + c) * _sectorsPerCluster;
ErrorNumber errno = _image.ReadSectors(_partition.Start + sectorStart,
false,
_sectorsPerCluster,
out byte[] clusterData,
out _);
if(errno != ErrorNumber.NoError)
{
AaruLogging.Debug(MODULE_NAME, "Error reading cluster {0}: {1}", clusterOffset + c, errno);
return errno;
}
long toCopy = Math.Min(_bytesPerCluster, dataSize - bytesRead);
Array.Copy(clusterData, 0, buf, bytesRead, toCopy);
bytesRead += toCopy;
}
}
return ErrorNumber.NoError;
}
}