WebpackPatcher: Avoid patching wrong instances (like Stripe)

This commit is contained in:
Nuckyz 2025-02-15 16:33:39 -03:00
parent 94d45780f9
commit 5ec564558e
No known key found for this signature in database
GPG key ID: 440BF8296E1C4AD9
4 changed files with 152 additions and 101 deletions

View file

@ -20,8 +20,7 @@ export async function loadLazyChunks() {
const invalidChunks = new Set<PropertyKey>();
const deferredRequires = new Set<PropertyKey>();
let chunksSearchingResolve: (value: void) => void;
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
const { promise: chunksSearchingDone, resolve: chunksSearchingResolve } = Promise.withResolvers<void>();
// True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>;

View file

@ -17,8 +17,7 @@ async function runReporter() {
try {
ReporterLogger.log("Starting test...");
let loadLazyChunksResolve: (value: void) => void;
const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r);
const { promise: loadLazyChunksDone, resolve: loadLazyChunksResolve } = Promise.withResolvers<void>();
// The main patch for starting the reporter chunk loading
addPatch({

View file

@ -17,6 +17,7 @@ import { AnyModuleFactory, AnyWebpackRequire, MaybePatchedModuleFactory, ModuleE
export const patches = [] as Patch[];
export const SYM_IS_PROXIED_FACTORY = Symbol("WebpackPatcher.isProxiedFactory");
export const SYM_ORIGINAL_FACTORY = Symbol("WebpackPatcher.originalFactory");
export const SYM_PATCHED_SOURCE = Symbol("WebpackPatcher.patchedSource");
export const SYM_PATCHED_BY = Symbol("WebpackPatcher.patchedBy");
@ -79,7 +80,8 @@ const define: typeof Reflect.defineProperty = (target, p, attributes) => {
};
// wreq.m is the Webpack object containing module factories. It is pre-populated with factories, and is also populated via webpackGlobal.push
// We use this setter to intercept when wreq.m is defined and apply patching to its factories.
// We use this setter to intercept when wreq.m is defined and setup our setters which decide whether we should patch these module factories
// and the Webpack instance where they are being defined.
// Factories can be patched in two ways. Eagerly or lazily.
// If we are patching eagerly, pre-populated factories are patched immediately and new factories are patched when set.
@ -97,7 +99,7 @@ define(Function.prototype, "m", {
set(this: AnyWebpackRequire, originalModules: AnyWebpackRequire["m"]) {
define(this, "m", { value: originalModules });
// Ensure this is one of Discord main Webpack instances.
// Ensure this is likely one of Discord main Webpack instances.
// We may catch Discord bundled libs, React Devtools or other extensions Webpack instances here.
const { stack } = new Error();
if (!stack?.includes("http") || stack.match(/at \d+? \(/) || !String(this).includes("exports:{}")) {
@ -105,53 +107,90 @@ define(Function.prototype, "m", {
}
const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1];
logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`);
allWebpackInstances.add(this);
// Define a setter for the ensureChunk property of WebpackRequire. Only the main Webpack (which is the only that includes chunk loading) has this property.
// So if the setter is called, this means we can initialize the internal references to WebpackRequire.
define(this, "e", {
// Define a setter for the bundlePath property of WebpackRequire. Only Webpack instances which include chunk loading functionality,
// like the main Discord Webpack, have this property.
// So if the setter is called with the Discord bundlePath, this means we should patch this instance and initialize the internal references to WebpackRequire.
define(this, "p", {
enumerable: false,
set(this: WebpackRequire, ensureChunk: WebpackRequire["e"]) {
define(this, "e", { value: ensureChunk });
clearTimeout(setterTimeout);
set(this: AnyWebpackRequire, bundlePath: NonNullable<AnyWebpackRequire["p"]>) {
define(this, "p", { value: bundlePath });
clearTimeout(bundlePathTimeout);
logger.info("Main WebpackInstance found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire");
_initWebpack(this);
if (bundlePath !== "/assets/") {
return;
}
patchThisInstance();
if (wreq == null && this.c != null) {
logger.info("Main WebpackInstance found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire");
_initWebpack(this as WebpackRequire);
}
}
});
// setImmediate to clear this property setter if this is not the main Webpack.
// If this is the main Webpack, wreq.e will always be set before the timeout runs.
const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "e"), 0);
// Patch the pre-populated factories
for (const moduleId in originalModules) {
const originalFactory = originalModules[moduleId];
// In the past, the sentry Webpack instance which we also wanted to patch used to rely on chunks being loaded before initting sentry.
// This Webpack instance did not include actual chunk loading, and only awaited for them to be loaded, which means it did not include the bundlePath property.
// To keep backwards compability, in case this is ever the case again, and keep patching this type of instance, we explicity patch instances which include wreq.O and not wreq.p.
// Since we cannot check what is the bundlePath of the instance to filter for the Discord bundlePath, we only patch it if wreq.p is not included,
// which means the instance relies on another instance which does chunk loading, and that makes it very likely to only target Discord Webpack instances like the old sentry.
if (updateExistingFactory(originalModules, moduleId, originalFactory, originalModules, true)) {
continue;
// Instead of patching when wreq.O is defined, wait for when wreq.O.j is defined, since that will be one of the last things to happen,
// which can assure wreq.p could have already been defined before.
define(this, "O", {
enumerable: false,
set(this: AnyWebpackRequire, onChunksLoaded: NonNullable<AnyWebpackRequire["O"]>) {
define(this, "O", { value: onChunksLoaded });
clearTimeout(onChunksLoadedTimeout);
const wreq = this;
define(onChunksLoaded, "j", {
enumerable: false,
set(this: NonNullable<AnyWebpackRequire["O"]>, j: NonNullable<AnyWebpackRequire["O"]>["j"]) {
define(this, "j", { value: j });
if (wreq.p == null) {
patchThisInstance();
}
}
});
}
notifyFactoryListeners(moduleId, originalFactory);
const proxiedFactory = new Proxy(Settings.eagerPatches ? patchFactory(moduleId, originalFactory) : originalFactory, moduleFactoryHandler);
define(originalModules, moduleId, { value: proxiedFactory });
}
define(originalModules, Symbol.toStringTag, {
value: "ModuleFactories",
enumerable: false
});
const proxiedModuleFactories = new Proxy(originalModules, moduleFactoriesHandler);
/*
If Webpack ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype
Reflect.setPrototypeOf(originalModules, new Proxy(originalModules, moduleFactoriesHandler));
*/
// If neither of these properties setters were triggered, delete them as they are not needed anymore.
const bundlePathTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0);
const onChunksLoadedTimeout = setTimeout(() => Reflect.deleteProperty(this, "O"), 0);
define(this, "m", { value: proxiedModuleFactories });
/**
* Patch the current Webpack instance assigned to `this` context.
* This should only be called if this instance was later found to be one we need to patch.
*/
const patchThisInstance = () => {
logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`);
allWebpackInstances.add(this);
// Proxy (and maybe patch) pre-populated factories
for (const moduleId in originalModules) {
updateExistingOrProxyFactory(originalModules, moduleId, originalModules[moduleId], originalModules, true);
}
define(originalModules, Symbol.toStringTag, {
value: "ModuleFactories",
enumerable: false
});
const proxiedModuleFactories = new Proxy(originalModules, moduleFactoriesHandler);
/*
If Webpack ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype
Reflect.setPrototypeOf(originalModules, new Proxy(originalModules, moduleFactoriesHandler));
*/
define(this, "m", { value: proxiedModuleFactories });
};
}
});
@ -172,93 +211,102 @@ const moduleFactoriesHandler: ProxyHandler<AnyWebpackRequire["m"]> = {
},
*/
set(target, p, newValue, receiver) {
if (updateExistingFactory(target, p, newValue, receiver)) {
return true;
}
notifyFactoryListeners(p, newValue);
const proxiedFactory = new Proxy(Settings.eagerPatches ? patchFactory(p, newValue) : newValue, moduleFactoryHandler);
return Reflect.set(target, p, proxiedFactory, receiver);
}
set: updateExistingOrProxyFactory
};
// The proxy for patching lazily and/or running factories with our wrapper.
const moduleFactoryHandler: ProxyHandler<MaybePatchedModuleFactory> = {
apply(target, thisArg: unknown, argArray: Parameters<AnyModuleFactory>) {
// SAFETY: Factories have `name` as their key in the module factories object, and that is always their module id
const moduleId = target.name;
// SYM_ORIGINAL_FACTORY means the factory has already been patched
if (target[SYM_ORIGINAL_FACTORY] != null) {
return runFactoryWithWrap(moduleId, target as PatchedModuleFactory, thisArg, argArray);
return runFactoryWithWrap(target as PatchedModuleFactory, thisArg, argArray);
}
// SAFETY: Factories have `name` as their key in the module factories object, and that is always their module id
const moduleId: string = target.name;
const patchedFactory = patchFactory(moduleId, target);
return runFactoryWithWrap(moduleId, patchedFactory, thisArg, argArray);
return runFactoryWithWrap(patchedFactory, thisArg, argArray);
},
get(target, p, receiver) {
if (target[SYM_ORIGINAL_FACTORY] != null && (p === SYM_PATCHED_SOURCE || p === SYM_PATCHED_BY)) {
return Reflect.get(target[SYM_ORIGINAL_FACTORY], p, target[SYM_ORIGINAL_FACTORY]);
if (p === SYM_IS_PROXIED_FACTORY) {
return true;
}
const v = Reflect.get(target, p, receiver);
const originalFactory: AnyModuleFactory = target[SYM_ORIGINAL_FACTORY] ?? target;
// Make proxied factories `toString` return their original factory `toString`
if (p === "toString") {
return v.bind(target[SYM_ORIGINAL_FACTORY] ?? target);
// Redirect these properties to the original factory, including making `toString` return the original factory `toString`
if (p === "toString" || p === SYM_PATCHED_SOURCE || p === SYM_PATCHED_BY) {
const v = Reflect.get(originalFactory, p, originalFactory);
return p === "toString" ? v.bind(originalFactory) : v;
}
return v;
return Reflect.get(target, p, receiver);
}
};
function updateExistingOrProxyFactory(moduleFactories: AnyWebpackRequire["m"], moduleId: PropertyKey, newFactory: AnyModuleFactory, receiver: any, ignoreExistingInTarget = false) {
if (updateExistingFactory(moduleFactories, moduleId, newFactory, receiver, ignoreExistingInTarget)) {
return true;
}
notifyFactoryListeners(moduleId, newFactory);
const proxiedFactory = new Proxy(Settings.eagerPatches ? patchFactory(moduleId, newFactory) : newFactory, moduleFactoryHandler);
return Reflect.set(moduleFactories, moduleId, proxiedFactory, receiver);
}
/**
* Update a factory that exists in any Webpack instance with a new original factory.
* Update a duplicated factory that exists in any of the Webpack instances we track with a new original factory.
*
* @param moduleFactoriesTarget The module factories where this new original factory is being set
* @param moduleFactories The module factories where this new original factory is being set
* @param moduleId The id of the module
* @param newFactory The new original factory
* @param receiver The receiver of the new factory
* @param ignoreExistingInTarget Whether to ignore checking if the factory already exists in the moduleFactoriesTarget
* @returns Whether the original factory was updated, or false if it doesn't exist in any Webpack instance
* @param receiver The receiver of the factory
* @param ignoreExistingInTarget Whether to ignore checking if the factory already exists in the moduleFactories where it is being set
* @returns Whether the original factory was updated, or false if it doesn't exist in any of the tracked Webpack instances
*/
function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], moduleId: PropertyKey, newFactory: AnyModuleFactory, receiver: any, ignoreExistingInTarget: boolean = false) {
let existingFactory: TypedPropertyDescriptor<AnyModuleFactory> | undefined;
function updateExistingFactory(moduleFactories: AnyWebpackRequire["m"], moduleId: PropertyKey, newFactory: AnyModuleFactory, receiver: any, ignoreExistingInTarget) {
let existingFactory: AnyModuleFactory | undefined;
let moduleFactoriesWithFactory: AnyWebpackRequire["m"] | undefined;
for (const wreq of allWebpackInstances) {
if (ignoreExistingInTarget && wreq.m === moduleFactoriesTarget) {
if (ignoreExistingInTarget && wreq.m === moduleFactories) {
continue;
}
if (Object.hasOwn(wreq.m, moduleId)) {
existingFactory = Reflect.getOwnPropertyDescriptor(wreq.m, moduleId);
existingFactory = wreq.m[moduleId];
moduleFactoriesWithFactory = wreq.m;
break;
}
}
if (existingFactory != null) {
// If existingFactory exists in any Webpack instance, it's either wrapped in our proxy, or it has already been required.
// In the case it is wrapped in our proxy, we need the Webpack instance with this new original factory to also have our proxy.
// So, define the descriptor of the existing factory on it.
if (moduleFactoriesWithFactory !== moduleFactoriesTarget) {
Reflect.defineProperty(receiver, moduleId, existingFactory);
// Sanity check to make sure these factories are equal
if (String(newFactory) !== String(existingFactory)) {
return false;
}
const existingFactoryValue = moduleFactoriesWithFactory![moduleId];
// If existingFactory exists in any of the Webpack instances we track, it's either wrapped in our proxy, or it has already been required.
// In the case it is wrapped in our proxy, and the instance we are setting does not already have it, we need to make sure the instance contains our proxy too.
if (moduleFactoriesWithFactory !== moduleFactories && existingFactory[SYM_IS_PROXIED_FACTORY]) {
Reflect.set(moduleFactories, moduleId, existingFactory, receiver);
}
// Else, if it is not wrapped in our proxy, set this new original factory in all the instances
else {
defineInWebpackInstances(moduleId, newFactory);
}
// Update with the new original factory, if it does have a current original factory
if (existingFactoryValue[SYM_ORIGINAL_FACTORY] != null) {
existingFactoryValue[SYM_ORIGINAL_FACTORY] = newFactory;
// Update existingFactory with the new original, if it does have a current original factory
if (existingFactory[SYM_ORIGINAL_FACTORY] != null) {
existingFactory[SYM_ORIGINAL_FACTORY] = newFactory;
}
// Persist patched source and patched by in the new original factory
if (IS_DEV) {
newFactory[SYM_PATCHED_SOURCE] = existingFactoryValue[SYM_PATCHED_SOURCE];
newFactory[SYM_PATCHED_BY] = existingFactoryValue[SYM_PATCHED_BY];
newFactory[SYM_PATCHED_SOURCE] = existingFactory[SYM_PATCHED_SOURCE];
newFactory[SYM_PATCHED_BY] = existingFactory[SYM_PATCHED_BY];
}
return true;
@ -267,6 +315,18 @@ function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], mo
return false;
}
/**
* Define a module factory in all the Webpack instances we track.
*
* @param moduleId The id of the module
* @param factory The factory
*/
function defineInWebpackInstances(moduleId: PropertyKey, factory: AnyModuleFactory) {
for (const wreq of allWebpackInstances) {
define(wreq.m, moduleId, { value: factory });
}
}
/**
* Notify all factory listeners.
*
@ -286,12 +346,11 @@ function notifyFactoryListeners(moduleId: PropertyKey, factory: AnyModuleFactory
/**
* Run a (possibly) patched module factory with a wrapper which notifies our listeners.
*
* @param moduleId The id of the module
* @param patchedFactory The (possibly) patched module factory
* @param thisArg The `value` of the call to the factory
* @param argArray The arguments of the call to the factory
*/
function runFactoryWithWrap(moduleId: PropertyKey, patchedFactory: PatchedModuleFactory, thisArg: unknown, argArray: Parameters<MaybePatchedModuleFactory>) {
function runFactoryWithWrap(patchedFactory: PatchedModuleFactory, thisArg: unknown, argArray: Parameters<MaybePatchedModuleFactory>) {
const originalFactory = patchedFactory[SYM_ORIGINAL_FACTORY];
if (patchedFactory === originalFactory) {
@ -299,25 +358,23 @@ function runFactoryWithWrap(moduleId: PropertyKey, patchedFactory: PatchedModule
delete patchedFactory[SYM_ORIGINAL_FACTORY];
}
// Restore the original factory in all the module factories objects, discarding our proxy and allowing it to be garbage collected
for (const wreq of allWebpackInstances) {
define(wreq.m, moduleId, { value: originalFactory });
}
let [module, exports, require] = argArray;
// Restore the original factory in all the module factories objects, discarding our proxy and allowing it to be garbage collected
defineInWebpackInstances(module.id, originalFactory);
if (wreq == null) {
if (!wreqFallbackApplied) {
wreqFallbackApplied = true;
// Make sure the require argument is actually the WebpackRequire function
if (typeof require === "function" && require.m != null) {
if (typeof require === "function" && require.m != null && require.c != null) {
const { stack } = new Error();
const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1];
logger.warn(
"WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called wrapped module factory (" +
`id: ${String(moduleId)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` +
`id: ${String(module.id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` +
")"
);
@ -355,8 +412,8 @@ function runFactoryWithWrap(moduleId: PropertyKey, patchedFactory: PatchedModule
if (shouldIgnoreModule) {
if (require.c != null) {
Object.defineProperty(require.c, moduleId, {
value: require.c[moduleId],
Object.defineProperty(require.c, module.id, {
value: require.c[module.id],
enumerable: false,
configurable: true,
writable: true
@ -369,7 +426,7 @@ function runFactoryWithWrap(moduleId: PropertyKey, patchedFactory: PatchedModule
for (const callback of moduleListeners) {
try {
callback(exports, moduleId);
callback(exports, module.id);
} catch (err) {
logger.error("Error in Webpack module listener:\n", err, callback);
}
@ -379,7 +436,7 @@ function runFactoryWithWrap(moduleId: PropertyKey, patchedFactory: PatchedModule
try {
if (filter(exports)) {
waitForSubscriptions.delete(filter);
callback(exports, moduleId);
callback(exports, module.id);
continue;
}
@ -392,7 +449,7 @@ function runFactoryWithWrap(moduleId: PropertyKey, patchedFactory: PatchedModule
if (exportValue != null && filter(exportValue)) {
waitForSubscriptions.delete(filter);
callback(exportValue, moduleId);
callback(exportValue, module.id);
break;
}
}

View file

@ -97,10 +97,6 @@ export const moduleListeners = new Set<CallbackFn>();
export const factoryListeners = new Set<FactoryListernFn>();
export function _initWebpack(webpackRequire: WebpackRequire) {
if (webpackRequire.c == null) {
return;
}
wreq = webpackRequire;
cache = webpackRequire.c;