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
6 changes: 6 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ export function markMovable(value: object): void {
})
}

// Copy own properties onto a prototype-less object, so that options are never
// resolved through a polluted prototype chain.
export function withNullPrototype<T extends object>(source: T): T {
return Object.assign(Object.create(null), source)
}

export interface Transferable {
readonly [kTransferable]: object
readonly [kValue]: object
Expand Down
23 changes: 14 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
isTransferable,
markMovable,
isMovable,
withNullPrototype,
kTransferable,
kValue,
type TinypoolData,
Expand Down Expand Up @@ -656,12 +657,12 @@ class ThreadPool {
? maybeFileURLToPath(options.filename)
: null

this.options = Object.assign(
Object.create(null),
kDefaultOptions,
options,
{ filename, maxQueue: 0 }
)
this.options = withNullPrototype({
...kDefaultOptions,
...options,
filename,
maxQueue: 0,
})

// The >= and <= could be > and < but this way we get 100 % coverage 🙃
if (
Expand Down Expand Up @@ -1108,7 +1109,8 @@ class ThreadPool {
await Promise.all(exitEvents)
}

async recycleWorkers(options: Pick<Options, 'runtime'> = {}) {
async recycleWorkers(_options: Pick<Options, 'runtime'> = {}) {
const options = withNullPrototype(_options)
const runtimeChanged =
options?.runtime && options.runtime !== this.options.runtime

Expand Down Expand Up @@ -1148,7 +1150,9 @@ class ThreadPool {
class Tinypool extends EventEmitterAsyncResource {
#pool: ThreadPool

constructor(options: Options = {}) {
constructor(_options: Options = {}) {
const options = withNullPrototype(_options)

// convert fractional option values to int
if (
options.minThreads !== undefined &&
Expand Down Expand Up @@ -1187,7 +1191,8 @@ class Tinypool extends EventEmitterAsyncResource {
}

run(task: any, options: RunOptions = kDefaultRunOptions) {
const { transferList, filename, name, signal, runtime, channel } = options
const { transferList, filename, name, signal, runtime, channel } =
withNullPrototype(options)

return this.#pool.runTask(task, {
transferList,
Expand Down
22 changes: 22 additions & 0 deletions test/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,28 @@ test('ignores worker options from prototype', async () => {
expect(result).toBe(42)
})

test('ignores worker filename from prototype', async () => {
{
const failsWhenLoaded = resolve(__dirname, 'fixtures/fails-when-loaded.mjs')

onTestFinished(() => {
// @ts-expect-error -- intentional
delete Object.prototype.filename
})

// @ts-expect-error -- intentional
Object.prototype.filename = failsWhenLoaded
}

const worker = new Tinypool({
filename: resolve(__dirname, 'fixtures/eval.js'),
})
const result = await worker.run('42', {
signal: new AbortController().signal,
})
expect(result).toBe(42)
})

vi.mock(import('node:os'), async (importOriginal) => {
const original = await importOriginal()
return {
Expand Down
Loading