ksmbd: add AAPL READDIR_ATTR V2 support

Extends the existing V1 inline-FinderInfo mechanism
(SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR) with the V2 variant:
byte-identical layout otherwise, except the ShortNameLength+Reserved
bytes (ignored outright by V1 clients) become a single flags field that
V2 clients actually interpret.

Negotiation: when a client's own client_caps requests V2
(SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2), advertise V2 instead of
V1 in the server's own server_caps reply -- they're mutually exclusive
on the wire, not both set together.

Wire format: the only currently-defined V2 flag,
AAPL_READDIR_ATTR_V2_NO_XATTR, signals that an item has no
xattrs/streams so the client can skip a separate query. Compute this
per-entry in ksmbd_vfs_fill_dentry_attrs() by checking for any xattr
under the XATTR_NAME_STREAM ("user.DosStream.") prefix -- a reliable,
distinct marker for genuine ADS/stream xattrs, unlike DOSATTRIB or ACL
xattrs which live under different prefixes, so this can't
false-positive into telling Finder a file has no extra data when it
actually does. Only computed when a V2 connection is active, to avoid
the extra listxattr() call otherwise.

V1's fixed ShortNameLength=24 convention (real macOS clients ignore
the value outright per the same client source, so it's cosmetic
parity with other real servers, not a functional requirement) is kept
V1-only rather than reused as a V2 base value -- V2 clients do
interpret this field, so it needs a clean 0-or-flag value, not a
leftover V1 constant that happens not to collide with the one defined
flag bit today.

Confirmed via live diagnostics that macOS actually negotiates and
uses V2 (client_caps bit 0x10 set) rather than falling back to V1 or
ignoring the capability.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Gael Blivet <gael.blivet@gmail.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
This commit is contained in:
Gael Blivet 2026-07-10 08:39:55 +09:00 committed by Namjae Jeon
parent aa38147e4f
commit 9907ed8457
8 changed files with 97 additions and 5 deletions

View File

@ -1261,6 +1261,14 @@ struct create_mxac_req {
#define SMB2_CRTCTX_AAPL_SUPPORTS_OSX_COPYFILE 2
#define SMB2_CRTCTX_AAPL_UNIX_BASED 4
#define SMB2_CRTCTX_AAPL_SUPPORTS_NFS_ACE 8
/*
* V2 extends the same inline-FinderInfo mechanism as
* SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR with an added flags field,
* confirmed byte-identical to V1 otherwise against AAPL's actual
* public client behavior. Mutually exclusive with the V1 bit on
* the wire, not both set together.
*/
#define SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2 16
/* "AAPL" Volume Capabilities bitmap */
#define SMB2_CRTCTX_AAPL_SUPPORT_RESOLVE_ID 1

View File

@ -125,6 +125,7 @@ struct ksmbd_conn {
atomic_t refcnt;
bool is_aapl;
bool aapl_readdir_attr; /* READDIR_ATTR negotiated */
bool aapl_readdir_attr_v2; /* V2 specifically */
struct work_struct release_work;
};

View File

@ -2158,11 +2158,15 @@ void create_posix_rsp_buf(char *cc, struct ksmbd_file *fp)
*
* Sending reply_bitmap with MODEL_INFO set but no model string causes
* smbfs.kext to enter a broken disconnect path requiring a macOS reboot.
* @readdir_attr_v2: advertise SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2
* instead of the V1 bit
*/
void create_aapl_rsp_buf(char *cc, __u64 vol_caps, __u64 req_bitmap)
void create_aapl_rsp_buf(char *cc, __u64 vol_caps, __u64 req_bitmap,
bool readdir_attr_v2)
{
struct create_aapl_rsp *buf;
u64 reply_bitmap;
u64 server_caps;
u32 data_len;
buf = (struct create_aapl_rsp *)cc;
@ -2188,8 +2192,12 @@ void create_aapl_rsp_buf(char *cc, __u64 vol_caps, __u64 req_bitmap)
buf->cmd = cpu_to_le32(SMB2_CRTCTX_AAPL_SERVER_QUERY);
buf->reply_bitmap = cpu_to_le64(reply_bitmap);
server_caps = AAPL_SERVER_CAPS_KSMBD;
if (readdir_attr_v2)
server_caps = (server_caps & ~SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR) |
SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2;
buf->server_caps = (reply_bitmap & SMB2_CRTCTX_AAPL_SERVER_CAPS) ?
cpu_to_le64(AAPL_SERVER_CAPS_KSMBD) : 0;
cpu_to_le64(server_caps) : 0;
buf->vol_caps = (reply_bitmap & SMB2_CRTCTX_AAPL_VOLUME_CAPS) ?
cpu_to_le64(vol_caps) : 0;

View File

@ -125,7 +125,8 @@ void create_durable_v2_rsp_buf(char *cc, struct ksmbd_file *fp);
void create_mxac_rsp_buf(char *cc, int maximal_access);
void create_disk_id_rsp_buf(char *cc, __u64 file_id, __u64 vol_id);
void create_posix_rsp_buf(char *cc, struct ksmbd_file *fp);
void create_aapl_rsp_buf(char *cc, __u64 vol_caps, __u64 req_bitmap);
void create_aapl_rsp_buf(char *cc, __u64 vol_caps, __u64 req_bitmap,
bool readdir_attr_v2);
struct create_context *smb2_find_context_vals(void *open_req, const char *tag, int tag_len);
struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn,
char *lease_key);

View File

@ -4413,12 +4413,23 @@ int smb2_open(struct ksmbd_work *work)
if (aapl_ctxt) {
if (aapl_client_caps & SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR)
conn->aapl_readdir_attr = true;
/*
* V2 extends the same inline-FinderInfo mechanism (see
* smb2pdu.h), so a V2-requesting client also gets
* aapl_readdir_attr treatment -- the reply just advertises
* the V2 bit instead of the V1 one (create_aapl_rsp_buf).
*/
if (aapl_client_caps & SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2) {
conn->aapl_readdir_attr = true;
conn->aapl_readdir_attr_v2 = true;
}
contxt_cnt++;
create_aapl_rsp_buf(rsp->Buffer +
le32_to_cpu(rsp->CreateContextsLength),
SMB2_CRTCTX_AAPL_FULL_SYNC,
aapl_req_bitmap);
aapl_req_bitmap,
conn->aapl_readdir_attr_v2);
le32_add_cpu(&rsp->CreateContextsLength,
conn->vals->create_aapl_size);
iov_len += conn->vals->create_aapl_size;
@ -4762,6 +4773,11 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level,
* the file extension for icon lookup)
* Reserved2 = Unix mode bits (uint16 LE)
* Reparse-point tag is indicated via ExtFileAttributes, not EaSize.
*
* V2 (conn->aapl_readdir_attr_v2): ShortNameLength+Reserved
* are read as a single flags field instead of being ignored
* -- see smb2pdu.h for the wire-format confirmation and
* AAPL_READDIR_ATTR_V2_NO_XATTR's meaning.
*/
__le32 reparse_tag =
smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode);
@ -4791,7 +4807,20 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level,
* interpreted; V1 doesn't. Either value is safe
* here, so keep 24 for parity.
*/
fibdinfo->ShortNameLength = 24;
if (conn->aapl_readdir_attr_v2) {
/*
* V2 repurposes this field as flags (see comment
* above) -- 24 is a V1-only convention that real
* macOS clients ignore outright, so don't reuse it
* here as a base value for a field V2 clients
* actually interpret.
*/
fibdinfo->ShortNameLength = 0;
if (!ksmbd_kstat->has_ads_stream)
fibdinfo->ShortNameLength = AAPL_READDIR_ATTR_V2_NO_XATTR;
} else {
fibdinfo->ShortNameLength = 24;
}
memset(fibdinfo->ShortName, 0, sizeof(fibdinfo->ShortName));
fibdinfo->Reserved2 = cpu_to_le16(ksmbd_kstat->kstat->mode & 0xffff);
} else {

View File

@ -95,6 +95,21 @@ struct preauth_integrity_info {
SMB2_CRTCTX_AAPL_SUPPORTS_OSX_COPYFILE | \
SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR)
/*
* READDIR_ATTR_V2 (SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2, see
* fs/smb/common/smb2pdu.h) extends the same inline-FinderInfo mechanism
* above with a flags field, confirmed byte-identical to V1 otherwise
* against AAPL's actual public client behavior. When a client's own
* client_caps requests V2, the server advertises V2 instead of V1 in
* its own server_caps reply; V1 and V2 are mutually exclusive on the
* wire, not both set together. The wire format's ShortNameLength+Reserved
* (ignored in V1) become a single flags field in V2 --
* AAPL_READDIR_ATTR_V2_NO_XATTR is the only flag bit currently defined,
* signaling the item has no xattrs/streams so the client can skip a
* separate query.
*/
#define AAPL_READDIR_ATTR_V2_NO_XATTR 0x01
/* Model string: up to 31 ASCII chars */
#define AAPL_MODEL_MAX_CHARS 31
#define AAPL_MODEL_UTF16_BYTES (AAPL_MODEL_MAX_CHARS * 2)

View File

@ -1684,6 +1684,35 @@ int ksmbd_vfs_fill_dentry_attrs(struct ksmbd_work *work,
}
}
/*
* Only pay for this when it'll actually be used: AAPL
* READDIR_ATTR_V2's flags field (AAPL_READDIR_ATTR_V2_NO_XATTR) is
* the only consumer. XATTR_NAME_STREAM ("user.DosStream.") is a
* reliable, distinct prefix for genuine ADS/stream xattrs -- unlike
* DOSATTRIB or ACL xattrs, which live under different prefixes, so
* this can't false-positive into telling Finder a file has no extra
* data when it actually does.
*/
ksmbd_kstat->has_ads_stream = false;
if (work->conn->aapl_readdir_attr_v2) {
char *xattr_list = NULL, *name;
ssize_t xattr_list_len;
xattr_list_len = ksmbd_vfs_listxattr(dentry, &xattr_list);
if (xattr_list_len > 0) {
for (name = xattr_list;
name - xattr_list < xattr_list_len;
name += strlen(name) + 1) {
if (!strncmp(name, XATTR_NAME_STREAM,
XATTR_NAME_STREAM_LEN)) {
ksmbd_kstat->has_ads_stream = true;
break;
}
}
}
kvfree(xattr_list);
}
return 0;
}

View File

@ -70,6 +70,7 @@ struct ksmbd_kstat {
struct kstat *kstat;
unsigned long long create_time;
__le32 file_attributes;
bool has_ads_stream; /* AAPL READDIR_ATTR V2 xattr-presence flag */
};
int ksmbd_vfs_lock_parent(struct dentry *parent, struct dentry *child);