Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Tests/SakeKitTests/PatchTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ private func read(_ tree: URL) throws -> String {
try String(contentsOf: tree.appending(path: "dlls/ntdll/unix/loader.c"), encoding: .utf8)
}

@Test func theRepositoryCarriesTheEightPatchesAndSaysTheyAreNotMIT() throws {
@Test func theRepositoryCarriesTheNinePatchesAndSaysTheyAreNotMIT() throws {
let patcher = WinePatcher(directory: repositoryPatches)
let patches = try patcher.patches()

Expand All @@ -152,6 +152,7 @@ private func read(_ tree: URL) throws -> String {
"0006-winemac-give-D3DMetal-a-hosted-swapchain-for-a-window-it-does-not-own.patch",
"0007-winhttp-stub-WINHTTP_OPTION_DECOMPRESSION.patch",
"0008-winhttp-stub-WINHTTP_OPTION_IPV6_FAST_FALLBACK.patch",
"0009-ntdll-leave-AppleDouble-files-out-of-directory-listings.patch",
])
// Each one says what it does on its first line, which is where the reasoning starts,
// and names the module it changes the way a Wine commit does.
Expand Down
60 changes: 54 additions & 6 deletions docs/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,13 @@ and the renderer that draws it was still alive seventy-five seconds later.

## Two patches to ntdll

sake carries eight patches in `patches/`, all LGPL-2.1-or-later because all are derivatives
of Wine. The two in ntdll are this section's; they came from the prototype unchanged and go
in before configure. The four in winemac.drv arrived with Steam on 2026-09-20 and are in the
Steam section below, and the two in winhttp arrived with Minecraft Dungeons II on 2026-09-29
and are in the GDK section after it. The build side of patching is in `wine-build.md` and
the licence side in `licensing.md`.
sake carries nine patches in `patches/`, all LGPL-2.1-or-later because all are derivatives
of Wine. Two of the three in ntdll are this section's; they came from the prototype unchanged
and go in before configure. The four in winemac.drv arrived with Steam on 2026-09-20 and are
in the Steam section below, the two in winhttp arrived with Minecraft Dungeons II on
2026-09-29 and are in the GDK section after it, and the third in ntdll came with the same
game a day later and has the exFAT section after that. The build side of patching is in
`wine-build.md` and the licence side in `licensing.md`.

**sake measured both on 2026-09-19**, against its own engine and bottle, the day it started
carrying them. The prototype's numbers are kept beside sake's because they are the
Expand Down Expand Up @@ -478,6 +479,48 @@ game is the sign-in going through with both of the stand-in's hooks removed, whi
the rebuilt engine: Microsoft's sign-in, then `LoginWithSteam`, then character select, with
every reply the stand-in logged, 20 of them, a 200 and no option refused.

## A bottle on exFAT: the `._` files macOS writes are not the game's

Measured in sake on 2026-09-30 in the `ex` bottle, a symlink into a directory on an exFAT
disk, with Minecraft Dungeons II started through Steam. The game had reset its settings on
every launch since 2026-09-29, with the community stand-in and with sake's own runtime alike.

**macOS writes a second file beside nearly every file there.** exFAT cannot store extended
attributes itself — `getattrlist` reports `VOL_CAP_INT_EXTENDED_ATTR` unset for that disk and
set for the internal APFS volume — so macOS keeps a file's attributes in a 4096-byte
AppleDouble file named `._` and the file's own name. On this Mac a file is given one as soon as
it is written, because it is given `com.apple.provenance`: the game's saves were, and so was a
file written from a shell. The bottle held 6,259 of them. Wine lists these as ordinary files,
marked hidden because their names start with a dot, and its sorted listing puts each before
the file it belongs to.

**The game read one as its settings.** It lists `Saved\SaveGames\*.*`, opened
`._GlobalSaveDataDefault.sav`, read its 4096 bytes and never opened `GlobalSaveDataDefault.sav`
at all. It then showed SETTINGS FILE DAMAGED and sent the person through the initial setup
again, although the file it had written is sound: JSON with every byte one lower. With the
five companions in `SaveGames` removed by hand, the same file loaded and the game went
straight to play; its next save brought all five back. Steam's client in that bottle had been
syncing `._sharedconfig.vdf` to Steam Cloud as one of its configuration files: its
`logs/cloud_log.txt` reports it in sync nine times before the patch.

**`patches/0009` leaves such a file out of a directory listing** when the file it belongs to is
beside it and the volume has no native extended attributes. A `._` file on APFS, or one with
nothing beside it, is still listed, and a name asked for exactly is still found.

**How to tell it worked.** `WINEDEBUG=+file` prints `leaving out` and the name for each file
left out, and the listing after it holds none of them. On the rebuilt engine, the same day and
with the five companions back on disk, the game's listing of `SaveGames` left them out and
returned the five saves, it read `GlobalSaveDataDefault.sav`, and it started with no dialog.
Steam's next sync named `sharedconfig.vdf` alone and found nothing to download. In
`wine cmd /c dir`, a `._` file with nothing beside it on the exFAT disk and a `._` file on APFS
were both listed, and `rmdir /s /q` removed an exFAT directory holding two companions it had
not been shown, since macOS removes a companion with its file. Starting `cmd` in that bottle
left out 854 names.

Not measured: FAT and SMB volumes, which macOS treats the same way when they lack native
extended attributes, and whether Steam ever removes the copy of `._sharedconfig.vdf` its
cloud still holds.

## Killing wineserver leaves the prefix's own services running

**Measured in sake on 2026-09-20.** A title was started from the library and stopped again.
Expand Down Expand Up @@ -623,6 +666,11 @@ started. Cut `argv[0]` at its first `.exe` and check what that ends with.
- **`WINEDEBUG=+pid` before anything else with more than one process.** Without it every
trace prefix is a thread id, and four Chromium processes cannot be told apart. Learned on
Steam, 2026-09-20.
- **`WINEDEBUG=<program>:+<channel>` traces one process.** An option with a name and a colon in
front applies only where the executable has that name (`parse_options` in
`dlls/ntdll/unix/debug.c`), so a game started by Steam can be traced without Steam's own
processes. `-all,Dungeons-Win64-Shipping.exe:+pid,Dungeons-Win64-Shipping.exe:+file` in
Steam's environment gave 24 MB by the time the game showed its first dialog, 2026-09-30.
- **`+macdrv_d3dmtl` is D3DMetal's half of the conversation.** It is the channel of the glue
in `dlls/winemac.drv/d3dmetal.c`, the only code D3DMetal calls in Wine. A `get_win_data`
with no `create_metal_device` after it means winemac returned NULL, and the six calls of a
Expand Down
7 changes: 4 additions & 3 deletions docs/wine-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ Notes that cost time to find:

## The patches go in before configure

`patches/` holds the changes sake makes to Wine's own code — eight of them as of 2026-09-29,
two in ntdll, four in winemac.drv and two in winhttp, all LGPL-2.1-or-later rather than this
`patches/` holds the changes sake makes to Wine's own code — nine of them as of 2026-09-30,
three in ntdll, four in winemac.drv and two in winhttp, all LGPL-2.1-or-later rather than this
repository's MIT. Four are upstream Wine commits carried only until the CrossOver sources
sake builds catch up — two of the winemac.drv ones with wine-11.11, the winhttp ones with
wine-11.4 and wine-11.7 — one is the reference implementation attached to Wine bug 60263,
Expand Down Expand Up @@ -92,7 +92,8 @@ rest of it fits, and says nothing. Upstream's hunk for 0008 did that on 2026-09-
context is a case that CrossOver's `session_set_option` does not have yet, and it applied
cleanly inside `connect_query_option`, where it stubbed nothing. `WinePatcher` passes `-F0`,
so a hunk like that stops the build as one that applies to neither form of the tree. All
eight patches applied to pristine 26.3.0 files with `-F0` exactly as they did without it.
eight patches applied to pristine 26.3.0 files with `-F0` exactly as they did without it, and
so did 0009 when it was added on 2026-09-30.

**A build with no patches is stopped rather than allowed.** Wine without them configures,
compiles, installs and passes every check in this document. What it cannot do is start a
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
ntdll: leave AppleDouble files out of directory listings.

On a volume that cannot store extended attributes itself, macOS keeps a file's
attributes in a second file beside it, named "._" followed by the file's own name.
exFAT is such a volume. On the machine this was measured on, a file got one as soon as it
was written, because macOS tags what applications write with com.apple.provenance; the
bottle below held 6,259. readdir() returns these companions like any other name, so a
Windows program listing a directory takes them for files of its own.

Measured in sake on 2026-09-30, in a bottle whose drive_c is on an exFAT disk.
Minecraft Dungeons II lists its SaveGames directory with FindFirstFileExW("*.*"), and
the sorted listing puts "._GlobalSaveDataDefault.sav" before
"GlobalSaveDataDefault.sav". The game opened the first, read its 4096 bytes and never
opened the second, then said its settings file was damaged and reset every setting.
It did that on every launch, because macOS writes the companion again as soon as the
game saves. With the companions removed by hand the same settings file loaded, and the
next save brought them back. Steam's client in that bottle had been syncing
"._sharedconfig.vdf" to Steam Cloud as a configuration file.

A "._X" is left out only when an "X" is beside it and the volume's capabilities say it
has no native extended attributes (VOL_CAP_INT_EXTENDED_ATTR), which is when macOS
makes one. On APFS a "._X" is an ordinary file, and one with no "X" belongs to nothing,
so both stay visible and deletable. A name asked for exactly, with no wildcard, is
found as before. WINEDEBUG=+file names each one left out.

--- a/dlls/ntdll/unix/file.c
+++ b/dlls/ntdll/unix/file.c
@@ -2637,8 +2637,57 @@
return STATUS_SUCCESS;
}

+
+#if defined(HAVE_GETATTRLIST) && defined(ATTR_VOL_CAPABILITIES) && \
+ defined(VOL_CAPABILITIES_FORMAT) && defined(VOL_CAP_FMT_CASE_SENSITIVE) && \
+ defined(VOL_CAP_INT_EXTENDED_ATTR)

/***********************************************************************
+ * is_appledouble_file
+ *
+ * Checks whether a name in the current directory is an AppleDouble file: "._X" beside
+ * an "X", on a volume that cannot store extended attributes itself, where macOS keeps
+ * X's attributes in that file. native_xattr holds the volume's answer for one listing,
+ * -1 until it has been asked.
+ */
+static BOOL is_appledouble_file( const char *name, int *native_xattr )
+{
+ struct attrlist attr;
+ struct vol_caps caps;
+ struct statfs stfs;
+ struct stat st;
+
+ if (name[0] != '.' || name[1] != '_' || !name[2]) return FALSE;
+
+ if (*native_xattr == -1)
+ {
+ memset( &attr, 0, sizeof(attr) );
+ attr.bitmapcount = ATTR_BIT_MAP_COUNT;
+ attr.volattr = ATTR_VOL_INFO | ATTR_VOL_CAPABILITIES;
+ /* getattrlist() only answers for volume attributes at the volume's root */
+ if (statfs( ".", &stfs ) == -1 ||
+ getattrlist( stfs.f_mntonname, &attr, &caps, sizeof(caps), 0 ) == -1 ||
+ caps.size != sizeof(caps) ||
+ !(caps.caps.valid[VOL_CAPABILITIES_INTERFACES] & VOL_CAP_INT_EXTENDED_ATTR))
+ *native_xattr = 1;
+ else
+ *native_xattr = !!(caps.caps.capabilities[VOL_CAPABILITIES_INTERFACES] &
+ VOL_CAP_INT_EXTENDED_ATTR);
+ }
+ return !*native_xattr && !lstat( name + 2, &st );
+}
+
+#else
+
+static BOOL is_appledouble_file( const char *name, int *native_xattr )
+{
+ return FALSE;
+}
+
+#endif
+
+
+/***********************************************************************
* read_directory_readdir
*
* Read a directory using the POSIX readdir interface; helper for NtQueryDirectoryFile.
@@ -2648,6 +2697,7 @@
struct dirent *de;
NTSTATUS status = STATUS_NO_MEMORY;
DIR *dir = opendir( "." );
+ int native_xattr = -1;

if (!dir) return STATUS_NO_SUCH_FILE;

@@ -2656,6 +2706,11 @@
while ((de = readdir( dir )))
{
if (!strcmp( de->d_name, "." ) || !strcmp( de->d_name, ".." )) continue;
+ if (is_appledouble_file( de->d_name, &native_xattr ))
+ {
+ TRACE( "leaving out %s\n", debugstr_a(de->d_name) );
+ continue;
+ }
if (!append_entry( data, de->d_name, NULL, mask )) goto done;
}
status = STATUS_SUCCESS;
3 changes: 2 additions & 1 deletion patches/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ worked.

Three kinds of file live here, and the header of each says which it is:

- **sake's own** (`0001`, `0002`, `0006`): written here, against something measured here.
- **sake's own** (`0001`, `0002`, `0006`, `0009`): written here, against something measured
here.
- **Upstream Wine commits carried early** (`0003`, `0004`, `0007`, `0008`): named by hash
and author in the header, applied as upstream wrote them apart from hunks the header says
were moved. They exist because CrossOver's sources lag upstream, and **each is dropped the
Expand Down
Loading