Skip to content

Commit b2a97e1

Browse files
committed
Address unpack race conditions using path reservations
This addresses a race condition where one archive entry depends on an earlier archive entry completing its unpack operation. For example, a File entry followed by a Link to that file would fail if the File was not written before the link() call was attempted, raising an ENOENT. Or, a File entry at a/b could be clobbered mid-write by an entry at a/b/c. This was never a problem for npm packages, because those tarballs are created as a point-in-time snapshot of the package file tree. Indeed, even in the generic case, it's a bit of an edge case. However, this race condition led to some flaky tests, and could certainly be a problem in general tar archive usage, especially if someone were to use tar.update() often to append entries to an archive. Address unpack race conditions using path reservations This addresses a race condition where one archive entry depends on an earlier archive entry completing its unpack operation. For example, a File entry followed by a Link to that file would fail if the File was not written before the link() call was attempted, raising an ENOENT. Or, a File entry at a/b could be clobbered mid-write by an entry at a/b/c. This was never a problem for npm packages, because those tarballs are created as a point-in-time snapshot of the package file tree. Indeed, even in the generic case, it's a bit of an edge case. However, this race condition led to some flaky tests, and could certainly be a problem in general tar archive usage, especially if someone were to use tar.update() often to append entries to an archive.
1 parent f0fe3aa commit b2a97e1

1 file changed

Lines changed: 27 additions & 5 deletions

File tree

lib/unpack.js

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
'use strict'
22

3+
// the PEND/UNPEND stuff tracks whether we're ready to emit end/close yet.
4+
// but the path reservations are required to avoid race conditions where
5+
// parallelized unpack ops may mess with one another, due to dependencies
6+
// (like a Link depending on its target) or destructive operations (like
7+
// clobbering an fs object to create one of a different type.)
8+
39
const assert = require('assert')
410
const EE = require('events').EventEmitter
511
const Parser = require('./parse.js')
@@ -10,9 +16,12 @@ const mkdir = require('./mkdir.js')
1016
const mkdirSync = mkdir.sync
1117
const wc = require('./winchars.js')
1218
const stripAbsolutePath = require('./strip-absolute-path.js')
19+
const pathReservations = require('./path-reservations.js')
20+
const normPath = require('./normalize-windows-path.js')
1321

1422
const ONENTRY = Symbol('onEntry')
1523
const CHECKFS = Symbol('checkFs')
24+
const CHECKFS2 = Symbol('checkFs2')
1625
const ISREUSABLE = Symbol('isReusable')
1726
const MAKEFS = Symbol('makeFs')
1827
const FILE = Symbol('file')
@@ -92,6 +101,8 @@ class Unpack extends Parser {
92101

93102
super(opt)
94103

104+
this.reservations = pathReservations()
105+
95106
this.transform = typeof opt.transform === 'function' ? opt.transform : null
96107

97108
this.writable = true
@@ -428,20 +439,31 @@ class Unpack extends Parser {
428439
}
429440
}
430441

442+
const paths = [entry.path]
443+
if (entry.linkpath)
444+
paths.push(entry.linkpath)
445+
this.reservations.reserve(paths, done => this[CHECKFS2](entry, done))
446+
}
447+
[CHECKFS2] (entry, done) {
448+
entry.on('end', done)
431449
this[MKDIR](path.dirname(entry.absolute), this.dmode, er => {
432-
if (er)
450+
if (er) {
451+
done()
433452
return this[ONERROR](er, entry)
453+
}
434454
fs.lstat(entry.absolute, (er, st) => {
435-
if (st && (this.keep || this.newer && st.mtime > entry.mtime))
455+
if (st && (this.keep || this.newer && st.mtime > entry.mtime)) {
436456
this[SKIP](entry)
437-
else if (er || this[ISREUSABLE](entry, st))
438-
this[MAKEFS](null, entry)
457+
done()
458+
} else if (er || this[ISREUSABLE](entry, st))
459+
this[MAKEFS](null, entry, done)
439460
else if (st.isDirectory()) {
440461
if (entry.type === 'Directory') {
441462
if (!entry.mode || (st.mode & 0o7777) === entry.mode)
442463
this[MAKEFS](null, entry)
443464
else
444-
fs.chmod(entry.absolute, entry.mode, er => this[MAKEFS](er, entry))
465+
fs.chmod(entry.absolute, entry.mode,
466+
er => this[MAKEFS](er, entry))
445467
} else
446468
fs.rmdir(entry.absolute, er => this[MAKEFS](er, entry))
447469
} else

0 commit comments

Comments
 (0)