Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: CVE-2024-21538 by migrating to promisify-child-process #4658

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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: 1 addition & 2 deletions detox/local-cli/utils/frameworkUtils.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
const os = require('os');
const path = require('path');

const { spawn } = require('child-process-promise');
const fs = require('fs-extra');
const { spawn } = require('promisify-child-process');

const detox = require('../../internals');
const { getFrameworkDirPath, getXCUITestRunnerDirPath } = require('../../src/utils/environment');


const frameworkBuildScript = '../../scripts/build_local_framework.ios.sh';
const xcuitestBuildScript = '../../scripts/build_local_xcuitest.ios.sh';

Expand Down
6 changes: 3 additions & 3 deletions detox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"prettier": "^3.1.1",
"react-native": "0.76.3",
"react-native-codegen": "^0.0.8",
"typescript": "^5.3.3",
"typescript": "~5.3.3",
"wtfnode": "^0.9.1"
},
"dependencies": {
Expand All @@ -73,14 +73,14 @@
"bunyan-debug-stream": "^3.1.0",
"caf": "^15.0.1",
"chalk": "^4.0.0",
"child-process-promise": "^2.2.0",
"detox-copilot": "^0.0.27",
"detox-copilot": "^0.0.24",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failing tests might be related to this downgrade...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you try to update it @matinzd ?

"execa": "^5.1.1",
"find-up": "^5.0.0",
"fs-extra": "^11.0.0",
"funpermaproxy": "^1.1.0",
"glob": "^8.0.3",
"ini": "^1.3.4",
"promisify-child-process": "^4.1.2",
"jest-environment-emit": "^1.0.8",
"json-cycle": "^1.3.0",
"lodash": "^4.17.11",
Expand Down
1 change: 1 addition & 0 deletions detox/src/devices/common/drivers/android/exec/ADB.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ class ADB {
const _spawnOptions = {
...spawnOptions,
capture: ['stdout'],
encoding: 'utf8',
};
return spawnWithRetriesAndLogs(this.adbBin, _flags, _spawnOptions);
}
Expand Down
2 changes: 2 additions & 0 deletions detox/src/devices/common/drivers/android/exec/ADB.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ describe('ADB', () => {
retries: 3,
timeout: 45000,
capture: ['stdout'],
encoding: 'utf8',
};
await adb.install(deviceId, 'path/to/bin.apk');

Expand Down Expand Up @@ -213,6 +214,7 @@ describe('ADB', () => {
retries: 3,
timeout: 45000,
capture: ['stdout'],
encoding: 'utf8',
};
const binaryPath = '/mock-path/filename.mock';
await adb.install(deviceId, binaryPath);
Expand Down
11 changes: 6 additions & 5 deletions detox/src/devices/common/drivers/android/exec/BinaryExec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// @ts-nocheck
const spawn = require('child-process-promise').spawn;
const { spawn } = require('promisify-child-process');

const exec = require('../../../../../utils/childProcess').execWithRetriesAndLogs;

Expand Down Expand Up @@ -27,15 +26,17 @@ class BinaryExec {
}

async exec(command) {
return (await exec(`"${this.binary}" ${command._getArgsString()}`)).stdout;
const result = await exec(`"${this.binary}" ${command._getArgsString()}`);

return result.stdout;
}

spawn(command, stdout, stderr) {
return spawn(this.binary, command._getArgs(), { detached: true, stdio: ['ignore', stdout, stderr] });
return spawn(this.binary, command._getArgs(), { detached: true, encoding: 'utf8', stdio: ['ignore', stdout, stderr] });
}
}

module.exports = {
ExecCommand,
BinaryExec,
BinaryExec
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable node/no-extraneous-require */
describe('BinaryExec', () => {
const binaryPath = '/binary/mock';

Expand All @@ -12,10 +13,10 @@ describe('BinaryExec', () => {
}));
exec = require('../../../../../utils/childProcess').execWithRetriesAndLogs;

jest.mock('child-process-promise', () => ({
jest.mock('promisify-child-process', () => ({
spawn: jest.fn()
}));
spawn = require('child-process-promise').spawn;
spawn = require('promisify-child-process').spawn;

const { BinaryExec } = require('./BinaryExec');
binaryExec = new BinaryExec(binaryPath);
Expand Down Expand Up @@ -87,7 +88,7 @@ describe('BinaryExec', () => {
expect(spawn).toHaveBeenCalledWith(binaryPath, commandArgs, expect.anything());
});

it('should chain-return child-process-promise from spawn', async () => {
it('should chain-return promisify-child-process from spawn', async () => {
const childProcessPromise = Promise.resolve('mock result');
spawn.mockReturnValue(childProcessPromise);

Expand Down
14 changes: 7 additions & 7 deletions detox/src/devices/runtime/drivers/ios/SimulatorDriver.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// @ts-nocheck
const path = require('path');

const exec = require('child-process-promise').exec;
const _ = require('lodash');
const { exec } = require('promisify-child-process');

const temporaryPath = require('../../../../artifacts/utils/temporaryPath');
const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError');
Expand All @@ -16,7 +16,6 @@ const traceInvocationCall = require('../../../../utils/traceInvocationCall').bin

const IosDriver = require('./IosDriver');


/**
* @typedef SimulatorDriverDeps { DeviceDriverDeps }
* @property applesimutils { AppleSimUtils }
Expand Down Expand Up @@ -111,8 +110,9 @@ class SimulatorDriver extends IosDriver {
if (Number.isNaN(pid)) {
throw new DetoxRuntimeError({
message: `Failed to find a process corresponding to the app bundle identifier (${bundleId}).`,
hint: `Make sure that the app is running on the device (${udid}), visually or via CLI:\n` +
`xcrun simctl spawn ${this.udid} launchctl list | grep -F '${bundleId}'\n`,
hint:
`Make sure that the app is running on the device (${udid}), visually or via CLI:\n` +
`xcrun simctl spawn ${this.udid} launchctl list | grep -F '${bundleId}'\n`
});
} else {
log.info({}, `Found the app (${bundleId}) with process ID = ${pid}. Proceeding...`);
Expand Down Expand Up @@ -141,7 +141,7 @@ class SimulatorDriver extends IosDriver {
const xcuitestRunner = new XCUITestRunner({ runtimeDevice: { id: this.getExternalId(), _bundleId } });
let x = point?.x ?? 100;
let y = point?.y ?? 100;
let _pressDuration = pressDuration ? (pressDuration / 1000) : 1;
let _pressDuration = pressDuration ? pressDuration / 1000 : 1;
const traceDescription = actionDescription.longPress({ x, y }, _pressDuration);
return this.withAction(xcuitestRunner, 'coordinateLongPress', traceDescription, x.toString(), y.toString(), _pressDuration.toString());
}
Expand Down Expand Up @@ -210,7 +210,7 @@ class SimulatorDriver extends IosDriver {
await this.emitter.emit('createExternalArtifact', {
pluginId: 'screenshot',
artifactName: screenshotName || path.basename(tempPath, '.png'),
artifactPath: tempPath,
artifactPath: tempPath
});

return tempPath;
Expand All @@ -223,7 +223,7 @@ class SimulatorDriver extends IosDriver {
await this.emitter.emit('createExternalArtifact', {
pluginId: 'uiHierarchy',
artifactName: artifactName,
artifactPath: viewHierarchyURL,
artifactPath: viewHierarchyURL
});

return viewHierarchyURL;
Expand Down
2 changes: 1 addition & 1 deletion detox/src/ios/XCUITestRunner.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { exec } = require('child-process-promise');
const { exec } = require('promisify-child-process');

const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
const environment = require('../utils/environment');
Expand Down
4 changes: 2 additions & 2 deletions detox/src/ios/XCUITestRunner.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
const XCUITestRunner = require('./XCUITestRunner');

jest.mock('child-process-promise', () => {
jest.mock('promisify-child-process', () => {
return {
exec: jest.fn(),
};
});

const { exec } = jest.requireMock('child-process-promise');
const { exec } = jest.requireMock('promisify-child-process');
const environment = jest.requireMock('../utils/environment');

jest.mock('../utils/environment');
Expand Down
8 changes: 7 additions & 1 deletion detox/src/utils/childProcess/exec.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
// @ts-nocheck
const { exec } = require('child-process-promise');
const _ = require('lodash');
const { exec } = require('promisify-child-process');

const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
const rootLogger = require('../logger').child({ cat: ['child-process', 'child-process-exec'] });
const retry = require('../retry');

const execsCounter = require('./opsCounter');

/**
* Executes a command with retries and logs
* @param {*} bin - command to execute
* @param {*} options - options
* @returns {Promise<import('promisify-child-process').Output>}
*/
async function execWithRetriesAndLogs(bin, options = {}) {
const {
retries = 9,
Expand Down
36 changes: 18 additions & 18 deletions detox/src/utils/childProcess/exec.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ describe('Exec utils', () => {
jest.mock('../logger');
logger = require('../logger');

jest.mock('child-process-promise');
cpp = require('child-process-promise');
jest.mock('promisify-child-process');
cpp = require('promisify-child-process');

exec = require('./exec');
});
Expand Down Expand Up @@ -91,8 +91,8 @@ describe('Exec utils', () => {
args: `--argument 123`,
statusLogs: {
trying: 'trying status log',
successful: 'successful status log',
},
successful: 'successful status log'
}
};
await execWithRetriesAndLogs('bin', options);

Expand All @@ -110,8 +110,8 @@ describe('Exec utils', () => {
const options = {
args: `--argument 123`,
statusLogs: {
retrying: true,
},
retrying: true
}
};

logger.debug.mockClear();
Expand Down Expand Up @@ -233,20 +233,20 @@ describe('Exec utils', () => {
});

const returnSuccessfulWithValue = (value) => ({
stdout: JSON.stringify(value),
stderr: 'err',
childProcess: {
exitCode: 0
}
});
stdout: JSON.stringify(value),
stderr: 'err',
childProcess: {
exitCode: 0
}
});

const returnErrorWithValue = (value) => ({
stdout: 'out',
stderr: value,
childProcess: {
exitCode: 1
}
});
stdout: 'out',
stderr: value,
childProcess: {
exitCode: 1
}
});

function mockCppSuccessful(cpp) {
const successfulResult = returnSuccessfulWithValue('successful result');
Expand Down
25 changes: 21 additions & 4 deletions detox/src/utils/childProcess/spawn.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-nocheck
const { spawn } = require('child-process-promise');
const _ = require('lodash');
const { spawn } = require('promisify-child-process');

const rootLogger = require('../logger').child({ cat: ['child-process', 'child-process-spawn'] });
const { escape } = require('../pipeCommands');
Expand Down Expand Up @@ -32,7 +32,8 @@ async function spawnWithRetriesAndLogs(binary, flags, options = {}) {
let result;
await retry({ retries, interval }, async (tryCount, lastError) => {
_logSpawnRetrying(logger, tryCount, lastError);
result = await _spawnAndLog(logger, binary, flags, command, spawnOptions, tryCount);
result = _spawnAndLog(logger, binary, flags, command, spawnOptions, tryCount);
await result;
});
return result;
}
Expand Down Expand Up @@ -69,8 +70,20 @@ async function interruptProcess(childProcessPromise, schedule) {
function _spawnAndLog(logger, binary, flags, command, options, tryCount) {
const { logLevelPatterns, silent, ...spawnOptions } = { stdio: ['ignore', 'pipe', 'pipe'], ...options };
const cpPromise = spawn(binary, flags, spawnOptions);
const childProcess = cpPromise.childProcess = cpPromise;
const originalThen = cpPromise.then.bind(cpPromise);
const augmentPromise = (fn) => {
return typeof fn === 'function'
? (result) => fn(Object.assign(result, {
childProcess,
exitCode: childProcess.exitCode,
pid: childProcess.pid
}))
: fn;
};
cpPromise.then = (onFulfilled, onRejected) => originalThen(augmentPromise(onFulfilled), augmentPromise(onRejected));
cpPromise.catch = (onRejected) => cpPromise.then(undefined, onRejected);

const { childProcess } = cpPromise;
const { exitCode, stdout, stderr } = childProcess;

const _logger = logger.child({ cpid: childProcess.pid });
Expand All @@ -85,8 +98,12 @@ function _spawnAndLog(logger, binary, flags, command, options, tryCount) {
stderr && stderr.on('data', _spawnStderrLoggerFn(_logger, logLevelPatterns));
}

/**
*
* @param {import('promisify-child-process').Output} resultOrErr
*/
function onEnd(resultOrErr) {
const signal = resultOrErr.childProcess.signalCode || '';
const signal = resultOrErr.signal || '';
const { code } = resultOrErr;
const action = signal ? `terminated with ${signal}` : `exited with code #${code}`;

Expand Down
Loading