Normal view

There are new articles available, click to refresh the page.
Before yesterdayMain stream

macOS 26.0 Tahoe build 25A354 is incompatible with Mac Studio M3 Ultra

By: hoakley
18 September 2025 at 03:53

If you have a Mac Studio M3 Ultra and want to upgrade it to run macOS 26.0 Tahoe, then I’m afraid you’re going to have wait for Apple to build a new release that will install on your Mac.

I’m very grateful to Ken who has tried unsuccessfully to upgrade from 15.7 to 26.0. There are plenty of others reporting exactly the same: the upgrade goes well until towards the end, then aborts and the Mac is restarted back into 15.7. The problem seems to originate from an error in its neural engine driver.

Having just taken a look through a comparison between kernel extensions shipped with macOS 15.6 and 26.0, there are several Apple silicon hardware kexts that seem to have gone missing in 26.0, although whether that’s the cause only Apple’s engineers should know.

Apple is advising all those affected to put their Tahoe upgrade on pause until it releases a new build that does fully support the M3 Ultra. Until then, 15.7 is the limit for Apple’s most powerful and expensive Macs yet.

Emscripten Fetch 接口的一个潜在内存泄漏问题

7 May 2025 at 18:04

近日发现了一个非常刁钻的可能引起基于 Emscripten 编译的 WASM 程序内存泄漏的问题。Emscripten 工具链提供了 Fetch 功能模块,这个模块允许我们调用浏览器的 fetch 接口来进行网络访问。

一个使用 fetch 接口的简单例子是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>
#include <emscripten/fetch.h>

void downloadSucceeded(emscripten_fetch_t *fetch) {
printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
// The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
emscripten_fetch_close(fetch); // Free data associated with the fetch.
}

void downloadFailed(emscripten_fetch_t *fetch) {
printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
emscripten_fetch_close(fetch); // Also free data on failure.
}

int main() {
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
strcpy(attr.requestMethod, "GET");
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
attr.onsuccess = downloadSucceeded;
attr.onerror = downloadFailed;
emscripten_fetch(&attr, "myfile.dat");
}

Fetch API 提供了一些比较高阶的功能,一种一个比较重要的功能是,他可以将下载的内容缓存到 IndexDB 中,这个缓存机制能够突破浏览器自身的缓存大小的限制(一般超过 50MB 的文件浏览器的自动缓存机制会拒绝缓存)。但是这个缓存机制会导致内存泄漏。

1 泄漏产生的过程

在开头的例子中,我们需要再 onerror 和 onsuccess 回调中调用 emscripten_fetch_close 接口来关闭 fetch 指针代表的请求。在关闭过程中,fetch 使用的数据缓存区将会被回收。这个过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
EMSCRIPTEN_RESULT emscripten_fetch_close(emscripten_fetch_t* fetch) {
if (!fetch) {
return EMSCRIPTEN_RESULT_SUCCESS; // Closing null pointer is ok, same as with free().
}

// This function frees the fetch pointer so that it is invalid to access it anymore.
// Use a few key fields as an integrity check that we are being passed a good pointer to a valid
// fetch structure, which has not been yet closed. (double close is an error)
if (fetch->id == 0 || fetch->readyState > STATE_MAX) {
return EMSCRIPTEN_RESULT_INVALID_PARAM;
}

// This fetch is aborted. Call the error handler if the fetch was still in progress and was
// canceled in flight.
if (fetch->readyState != STATE_DONE && fetch->__attributes.onerror) {
fetch->status = (unsigned short)-1;
strcpy(fetch->statusText, "aborted with emscripten_fetch_close()");
fetch->__attributes.onerror(fetch);
}

fetch_free(fetch);
return EMSCRIPTEN_RESULT_SUCCESS;
}

可以看到,回收并非总会发生, emscripten_fetch_close 函数会对 fetch 的部分状态进行检查,如果检查失败,则会返回一个 EMSCRIPTEN_RESULT_INVALID_PARAM 的错误码,并且不会执行后续的清理过程(`fetch_free)。被检查的两属性中,fetch->id 是我们需要关注的对象。fetch->id 这个属性作为 fetch 的唯一标识符,是用来建立起 C++ 端的请求对象和 JS 端的请求对象的映射的。id 的值在 JS 端分配。查看源码中的 Fetch.js 文件,

1
2
3
4
5
6
7
8
9
10
11
12
function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) {
// ...

var id = Fetch.xhrs.allocate(xhr);
#if FETCH_DEBUG
dbg(`fetch: id=${id}`);
#endif
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.id, 'id', 'u32') }}};

// ...

}

这是唯一的一处 id 复制。这段代码位于 fetchXHR 函数中,这意味着只有发起了 XHR 请求时,id 才会被分配。那么,如果缓存存在呢?这时不会调用 fetchXHR 函数(而是调用 fetchLoadCachedData 函数)。这意味着回调函数中我们试图调用 emscripten_fetch_close 函数来关闭请求并回收资源时,这个回收过程无法进行,这导致了内存泄漏。

2 怎么解决这个问题

要解决这个问题我们只需要强行让 fetch->id == 0 的检查无法通过即可,我们可以在 emscripten_fetch_close 调用前,强行设置 fetch->id 为一个非零值。那么什么值合适呢?如果我们取值和已有的请求的 id 相同,那么 emscripten_fetch_close 可能将那个请求关闭。研究 id 分配的过程(即 Fetch.xhrs.allocate 的实现)

1
2
3
4
5
6
// libcore.js
allocate(handle) {
var id = this.freelist.pop() || this.allocated.length;
this.allocated[id] = handle;
return id;
}

可以看到,id 是顺序分配的,且使用过的 id 会被回收使用(freelist)。因此我们可以设置一个较大的值,只要同一时间最大的并发请求数量不超过这个值,那就是安全的。我一般选择设置为 0xffff。 那么,正确的关闭请求的方式是:

1
2
3
4
if (fetch->id == 0) {
fetch->id = 0xffff;
}
emscripten_fetch_close(fetch);
❌
❌