Fix macOS SMB mount: AAPL caps, credit grant, file_index, QueryDirectory padding

- AAPL: Restore UNIX_BASED+NFS_ACE server_caps, RESOLVE_ID+FULL_SYNC volume_caps (Samba baseline)
- Credit: Grant min 1 credit in dispatch response for smbclient compatibility
- file_index: Assign 1-based index per entry in list_dir (both VFS and local backends)
- smb_match(): Add wildcard pattern filter (*/?) for macOS single-entry QueryDirectory probes
- FILE_ID_BOTH_DIR_INFORMATION: Add 2-byte Reserved2 padding between ShortName and FileId
- macOS Sequoia 15.5 mount_smbfs now succeeds (tested: ls, cat, read)
This commit is contained in:
Warren
2026-06-23 09:44:01 +08:00
parent 8ef1406ed3
commit e7863a3034
6 changed files with 94 additions and 15 deletions

View File

@@ -285,18 +285,31 @@ impl Handle for VfsHandle {
}
}
async fn list_dir(&self, _pattern: Option<&str>) -> Result<Vec<DirEntry>, SmbError> {
async fn list_dir(&self, pattern: Option<&str>) -> Result<Vec<DirEntry>, SmbError> {
match self {
Self::File { .. } => Err(SmbError::NotADirectory),
Self::Directory { vfs, path } => {
let entries = vfs.read_dir(path).map_err(map_error)?;
let result = entries
let mut result: Vec<DirEntry> = entries
.into_iter()
.filter(|entry| {
let p = match pattern {
None => return true,
Some(p) => p,
};
if p == "*" || p == "*.*" || p.is_empty() {
return true;
}
smb_match(&entry.name, p)
})
.map(|entry| {
let info = vfs_stat_to_file_info(&entry.stat, &entry.name, path);
DirEntry { info }
})
.collect();
for (i, entry) in result.iter_mut().enumerate() {
entry.info.file_index = (i + 1) as u64;
}
Ok(result)
}
}
@@ -307,6 +320,36 @@ impl Handle for VfsHandle {
}
}
/// Simple SMB wildcard match: `*` matches any sequence, `?` matches one char.
fn smb_match(name: &str, pattern: &str) -> bool {
let name = name.as_bytes();
let pat = pattern.as_bytes();
let mut ni = 0;
let mut pi = 0;
let mut star_idx: Option<usize> = None;
let mut match_idx = 0;
while ni < name.len() {
if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == name[ni]) {
ni += 1;
pi += 1;
} else if pi < pat.len() && pat[pi] == b'*' {
star_idx = Some(pi);
match_idx = ni;
pi += 1;
} else if let Some(si) = star_idx {
pi = si + 1;
match_idx += 1;
ni = match_idx;
} else {
return false;
}
}
while pi < pat.len() && pat[pi] == b'*' {
pi += 1;
}
pi == pat.len()
}
fn filetime_to_systemtime(ft: u64) -> SystemTime {
if ft < FILETIME_OFFSET {
return SystemTime::UNIX_EPOCH;

View File

@@ -581,6 +581,8 @@ async fn build_response_bytes(
hdr.session_id = sid;
}
hdr.signature = [0u8; 16];
// Grant at least 1 credit so clients (e.g. Samba smbclient) can proceed.
hdr.credit_request_response = hdr.credit_request_response.max(1);
let request_was_signed = req_hdr.flags & SMB2_FLAGS_SIGNED != 0;
// MS-SMB2 §3.3.5.5.3 step 12: SessionSetup SUCCESS must be signed for

View File

@@ -694,7 +694,7 @@ impl Handle for LocalHandle {
LocalHandle::Dir { dir_handle, .. } => {
let dir_handle = Arc::clone(dir_handle);
let pat = pattern.map(|s| s.to_owned());
spawn_blocking(move || -> io::Result<Vec<SmbDirEntry>> {
let result: SmbResult<Vec<SmbDirEntry>> = spawn_blocking(move || -> io::Result<Vec<SmbDirEntry>> {
let mut out = Vec::new();
for entry in dir_handle.entries()? {
let entry = entry?;
@@ -703,22 +703,26 @@ impl Handle for LocalHandle {
continue;
};
if let Some(p) = pat.as_deref() {
if !(p.is_empty() || p == "*" || p == "*.*" || glob_match(p, &name)) {
let matches_glob = p.is_empty() || p == "*" || p == "*.*" || glob_match(p, &name);
if !matches_glob {
continue;
}
}
let md = entry.metadata()?;
let info = file_info_from_metadata(name, &md);
tracing::debug!(name = %info.name, is_dir = %info.is_directory, "list_dir entry");
out.push(SmbDirEntry { info });
}
tracing::debug!(count = %out.len(), "list_dir complete");
// Assign unique file_index (1-based) so SMB2 FileId differs per entry
for (i, entry) in out.iter_mut().enumerate() {
entry.info.file_index = (i + 1) as u64;
}
Ok(out)
})
.await
.map_err(join_to_io)
.map_err(io_to_smb)?
.map_err(io_to_smb)
.map_err(io_to_smb);
return result;
}
}
}

View File

@@ -414,10 +414,13 @@ pub async fn handle(
use crate::proto::messages::CreateContext;
use crate::proto::messages::{
AaplCreateContextRequest, AaplCreateContextResponse,
SMB2_CRTCTX_AAPL_SERVER_QUERY, SMB2_CRTCTX_AAPL_SERVER_CAPS,
SMB2_CRTCTX_AAPL_VOLUME_CAPS, SMB2_CRTCTX_AAPL_MODEL_INFO,
SMB2_CRTCTX_AAPL_UNIX_BASED, SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR,
SMB2_CRTCTX_AAPL_SERVER_QUERY,
SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR,
SMB2_CRTCTX_AAPL_UNIX_BASED,
SMB2_CRTCTX_AAPL_SUPPORTS_NFS_ACE,
SMB2_CRTCTX_AAPL_CASE_SENSITIVE,
SMB2_CRTCTX_AAPL_SUPPORT_RESOLVE_ID,
SMB2_CRTCTX_AAPL_FULL_SYNC,
};
let contexts = CreateContext::parse_chain(&req.create_contexts).unwrap_or_default();
@@ -426,8 +429,12 @@ pub async fn handle(
if let Some(ctx) = aapl_ctx {
if let Some(aapl_req) = AaplCreateContextRequest::from_bytes(&ctx.data) {
if aapl_req.command == SMB2_CRTCTX_AAPL_SERVER_QUERY {
let server_caps = SMB2_CRTCTX_AAPL_UNIX_BASED | SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR;
let volume_caps = SMB2_CRTCTX_AAPL_CASE_SENSITIVE;
let server_caps = SMB2_CRTCTX_AAPL_UNIX_BASED
| SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR
| SMB2_CRTCTX_AAPL_SUPPORTS_NFS_ACE;
let volume_caps = SMB2_CRTCTX_AAPL_CASE_SENSITIVE
| SMB2_CRTCTX_AAPL_SUPPORT_RESOLVE_ID
| SMB2_CRTCTX_AAPL_FULL_SYNC;
let aapl_resp = AaplCreateContextResponse::new_server_query(
aapl_req.request_bitmap,
aapl_req.client_caps,

View File

@@ -13,6 +13,13 @@ use crate::ntstatus;
use crate::server::ServerState;
use crate::utils::utf16le_to_string;
fn hex_dump(label: &str, data: &[u8], max: usize) {
let show = data.len().min(max);
let hex: Vec<String> = data[..show].iter().map(|b| format!("{:02x}", b)).collect();
tracing::debug!("{}: len={} hex=[{}] {}", label, data.len(), hex.join(" "),
if data.len() > max { format!("... (truncated {})", data.len()) } else { String::new() });
}
pub async fn handle(
_server: &Arc<ServerState>,
conn: &Arc<Connection>,
@@ -38,16 +45,24 @@ pub async fn handle(
};
let pattern_str = utf16le_to_string(&req.file_name);
let pattern: Option<String> = if pattern_str.is_empty() || pattern_str == "*" {
let is_empty_pat = pattern_str.is_empty();
let pattern: Option<String> = if is_empty_pat || pattern_str == "*" {
None
} else {
Some(pattern_str)
Some(pattern_str.clone())
};
let restart = req.flags & QueryDirectoryRequest::FLAG_RESTART_SCANS != 0
|| req.flags & QueryDirectoryRequest::FLAG_REOPEN != 0;
let single_entry = req.flags & QueryDirectoryRequest::FLAG_RETURN_SINGLE_ENTRY != 0;
tracing::debug!(
class = %req.file_information_class, output_buf_len = %req.output_buffer_length,
flags = %req.flags, restart = %restart, single = %single_entry,
pattern = %pattern_str, file_name_hex = ?req.file_name,
"QueryDirectory request"
);
// Populate or refresh the cursor.
{
let mut open = open_arc.write().await;
@@ -55,6 +70,8 @@ pub async fn handle(
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
if open.search_state.is_none() || restart {
let handle_type = open.handle.as_ref().map(|h| std::any::type_name_of_val(&*h));
tracing::debug!("handle type: {:?}", handle_type);
let entries = match open.handle.as_ref() {
Some(h) => h.list_dir(pattern.as_deref()).await,
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
@@ -63,6 +80,7 @@ pub async fn handle(
Ok(e) => e,
Err(e) => return HandlerResponse::err(e.to_nt_status()),
};
tracing::debug!(">>> list_dir count={}", entries.len());
open.search_state = Some(DirCursor {
entries,
next: 0,
@@ -85,6 +103,7 @@ pub async fn handle(
}
let entry = &cursor.entries[cursor.next];
let file_index = entry.info.file_index;
tracing::debug!(name = %entry.info.name, file_index = %file_index, "encode_dir_entry file_index");
let mut bytes = encode_dir_entry(class_byte, entry, file_index);
if bytes.is_empty() {
cursor.next += 1;
@@ -119,11 +138,14 @@ pub async fn handle(
break;
}
}
// Log entries processed
tracing::debug!(next = cursor.next, total = cursor.entries.len(), buf_len = buf.len(), cap = cap, "QueryDirectory encoding done");
}
if buf.is_empty() {
return HandlerResponse::err(ntstatus::STATUS_NO_MORE_FILES);
}
hex_dump("QueryDirectory buffer", &buf, 512);
// Set last entry's NextEntryOffset to 0 (no next entry)
if let Some(prev_off) = last_offset_pos {
buf[prev_off..prev_off + 4].copy_from_slice(&0u32.to_le_bytes());

View File

@@ -371,6 +371,7 @@ pub fn encode_dir_entry(class: u8, entry: &DirEntry, file_index: u64) -> Vec<u8>
out.push(0); // ShortNameLength
out.push(0); // Reserved1
out.extend_from_slice(&[0u8; 24]); // ShortName
out.extend_from_slice(&[0u8; 2]); // Reserved2 (alignment padding for FileId)
out.extend_from_slice(&file_index.to_le_bytes()); // FileId (8 bytes)
out.extend_from_slice(&name_u16);
out