-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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(ext/node): shared global buffer unlock correctness fix #20314
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -311,21 +311,37 @@ export class LibuvStreamWrap extends HandleWrap { | |
|
||
/** Internal method for reading from the attached stream. */ | ||
async #read() { | ||
const isOwnedBuf = bufLocked; | ||
let buf = bufLocked ? new Uint8Array(SUGGESTED_SIZE) : BUF; | ||
bufLocked = true; | ||
// Lock safety: We must hold this lock until we are certain that buf is no longer used | ||
// This setup code is a little verbose, but we need to be careful about buffer management | ||
let buf, locked = false; | ||
if (bufLocked) { | ||
// Already locked, allocate | ||
buf = new Uint8Array(SUGGESTED_SIZE); | ||
} else { | ||
// Not locked, take the buffer + lock | ||
buf = BUF; | ||
locked = bufLocked = true; | ||
Comment on lines
+317
to
+323
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't the code be much simpler if you always allocate Is the current shared buffer motivated only by a performance gain? @mmastrac |
||
} | ||
try { | ||
let nread: number | null; | ||
const ridBefore = this[kStreamBaseField]!.rid; | ||
try { | ||
nread = await this[kStreamBaseField]!.read(buf); | ||
} catch (e) { | ||
// Lock safety: we know that the buffer will not be used in this function again | ||
// All exits from this block either return or re-assign buf to a different value | ||
if (locked) { | ||
bufLocked = locked = false; | ||
} | ||
|
||
// Try to read again if the underlying stream resource | ||
// changed. This can happen during TLS upgrades (eg. STARTTLS) | ||
if (ridBefore != this[kStreamBaseField]!.rid) { | ||
return this.#read(); | ||
} | ||
|
||
buf = new Uint8Array(0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved this line for clarity |
||
|
||
if ( | ||
e instanceof Deno.errors.Interrupted || | ||
e instanceof Deno.errors.BadResource | ||
|
@@ -339,8 +355,6 @@ export class LibuvStreamWrap extends HandleWrap { | |
} else { | ||
nread = codeMap.get("UNKNOWN")!; | ||
} | ||
|
||
buf = new Uint8Array(0); | ||
} | ||
|
||
nread ??= codeMap.get("EOF")!; | ||
|
@@ -351,7 +365,17 @@ export class LibuvStreamWrap extends HandleWrap { | |
this.bytesRead += nread; | ||
} | ||
|
||
buf = isOwnedBuf ? buf.subarray(0, nread) : buf.slice(0, nread); | ||
// We release the lock early so a re-entrant read can make use of the shared buffer, but | ||
// we need to make a copy of the data in the shared buffer. | ||
if (locked) { | ||
// Lock safety: we know that the buffer will not be used in this function again | ||
// We're making a copy of data that lives in the shared buffer | ||
buf = buf.slice(0, nread); | ||
bufLocked = locked = false; | ||
} else { | ||
// The buffer isn't owned, so let's create a subarray view | ||
buf = buf.subarray(0, nread); | ||
} | ||
|
||
streamBaseState[kArrayBufferOffset] = 0; | ||
|
||
|
@@ -365,7 +389,10 @@ export class LibuvStreamWrap extends HandleWrap { | |
this.#read(); | ||
} | ||
} finally { | ||
bufLocked = false; | ||
// Lock safety: we know that the buffer will not be used in this function again | ||
if (locked) { | ||
bufLocked = locked = false; | ||
} | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that in the old code, p2 was guaranteed to fire before p so there wasn't much sense in waiting on both.