Skip to content

Commit b845baa

Browse files
committed
Fix crash in FileBufferedChannel
1 parent 500f5fe commit b845baa

3 files changed

Lines changed: 108 additions & 1 deletion

File tree

‎CHANGELOG‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Release 6.2.1 (Not yet released)
1212
* Fix reporting of application output printed during smart spawning preloader startup.
1313
* Include captured subprocess output in application spawn failure messages in logs, allowing easier troubleshooting without reading the HTML report.
1414
* `passenger-config reopen-logs` now reopens both stdout and stderr, and can exit successfully when Passenger is not running by passing `--ignore-passenger-not-running`. Closes GH-2259.
15+
* Fix an occasional Passenger core crash after serving a large response that Passenger generates itself, such as an application error page. Requests arriving while the core restarted failed with 502 Bad Gateway.
1516

1617

1718
Release 6.2.0

‎src/cxx_supportlib/ServerKit/FileBufferedChannel.h‎

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,53 @@ class FileBufferedChannel: protected Channel {
486486
FBC_DEBUG("pushBuffer() completed: nbuffers = " << nbuffers << ", bytesBuffered = " << bytesBuffered);
487487
}
488488

489+
/**
490+
* Like `pushBuffer()`, but if `buffer` merely wraps memory owned by the caller
491+
* (i.e. it has no mbuf_block that keeps its memory alive), then the data is first
492+
* copied into mbufs from the pool. Large buffers are split over multiple mbufs.
493+
*
494+
* Must be used for all buffers queued in the in-file mode. There, buffers stay
495+
* queued until the writer has moved them to the file, which may be long after the
496+
* reader has passed them to the underlying channel. Callers (e.g. HttpServer
497+
* writing a response body from the request's memory pool) only guarantee that
498+
* such memory stays valid until the reader has passed it on.
499+
*/
500+
void pushBufferWithOwnedMemory(const MemoryKit::mbuf &buffer) {
501+
if (buffer.mbuf_block != NULL || buffer.empty()) {
502+
pushBuffer(buffer);
503+
return;
504+
}
505+
506+
size_t chunkSize = MemoryKit::mbuf_pool_data_size(&ctx->mbuf_pool);
507+
const char *pos = buffer.start;
508+
while (pos < buffer.end) {
509+
size_t size = std::min<size_t>(buffer.end - pos, chunkSize);
510+
MemoryKit::mbuf copy = MemoryKit::mbuf_get(&ctx->mbuf_pool);
511+
memcpy(copy.start, pos, size);
512+
pushBuffer(MemoryKit::mbuf(copy, 0, size));
513+
pos += size;
514+
}
515+
}
516+
517+
/**
518+
* Called when switching to the in-file mode: ensures that buffers that were
519+
* queued during the in-memory mode don't reference caller-owned memory either.
520+
* See `pushBufferWithOwnedMemory()` for why.
521+
*/
522+
void takeOwnershipOfQueuedBuffers() {
523+
deque<MemoryKit::mbuf> buffers;
524+
if (nbuffers > 0) {
525+
buffers.push_back(firstBuffer);
526+
buffers.insert(buffers.end(), moreBuffers.begin(), moreBuffers.end());
527+
}
528+
clearBuffers(false);
529+
530+
deque<MemoryKit::mbuf>::const_iterator it, end = buffers.end();
531+
for (it = buffers.begin(); it != end; it++) {
532+
pushBufferWithOwnedMemory(*it);
533+
}
534+
}
535+
489536
void popBuffer() {
490537
assert(bytesBuffered >= firstBuffer.size());
491538
bytesBuffered -= firstBuffer.size();
@@ -836,6 +883,7 @@ class FileBufferedChannel: protected Channel {
836883
P_ASSERT_EQ(inFileMode, 0);
837884

838885
FBC_DEBUG("Switching to in-file mode");
886+
takeOwnershipOfQueuedBuffers();
839887
mode = IN_FILE_MODE;
840888
inFileMode = boost::make_shared<InFileMode>(ctx->libuv);
841889
createBufferFile();
@@ -1423,7 +1471,11 @@ class FileBufferedChannel: protected Channel {
14231471
FBC_DEBUG("Feeding aborted: EOF or error detected");
14241472
return;
14251473
}
1426-
pushBuffer(buffer);
1474+
if (mode == IN_FILE_MODE) {
1475+
pushBufferWithOwnedMemory(buffer);
1476+
} else {
1477+
pushBuffer(buffer);
1478+
}
14271479
if (mode == IN_MEMORY_MODE && passedThreshold()) {
14281480
switchToInFileMode();
14291481
} else if (mode == IN_FILE_MODE

‎test/cxx/ServerKit/FileBufferedChannelTest.cpp‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,23 @@ namespace tut {
220220
void _setChannelDataCallback(FileBufferedChannel::DataCallback callback) {
221221
channel.setDataCallback(callback);
222222
}
223+
224+
// Feeds a buffer that merely wraps caller-owned memory, like HttpServer does
225+
// with response bodies allocated from a request's memory pool. Once the reader
226+
// has passed the data on, the memory is freed, like HttpServer does when it
227+
// resets the pool at the end of the request.
228+
void feedCallerOwnedDataAndFreeItOncePassedOn(const string &data) {
229+
bg.safe->runSync(boost::bind(
230+
&ServerKit_FileBufferedChannelTest::_feedCallerOwnedDataAndFreeItOncePassedOn,
231+
this, data));
232+
}
233+
234+
void _feedCallerOwnedDataAndFreeItOncePassedOn(string data) {
235+
char *memory = new char[data.size()];
236+
memcpy(memory, data.data(), data.size());
237+
channel.feed(memory, data.size());
238+
delete[] memory;
239+
}
223240
};
224241

225242
DEFINE_TEST_GROUP_WITH_LIMIT(ServerKit_FileBufferedChannelTest, 100);
@@ -1255,4 +1272,41 @@ namespace tut {
12551272
result = log.find("Error: ") == 0;
12561273
);
12571274
}
1275+
1276+
TEST_METHOD(51) {
1277+
set_test_name("In the in-file mode, buffers that wrap caller-owned memory are "
1278+
"still moved to the file after the caller freed that memory, so the "
1279+
"writer must not read from the caller's memory");
1280+
1281+
// Detecting the use-after-free requires AddressSanitizer or Valgrind,
1282+
// which CI uses for the C++ tests.
1283+
//
1284+
// This is deterministic: the buffer file is created in the background, but
1285+
// its completion is processed on the event loop, which is busy with
1286+
// feeding and freeing until the data has been freed. Only then can the
1287+
// writer start moving the buffer to the file.
1288+
Json::Value config;
1289+
vector<ConfigKit::Error> errors;
1290+
config["file_buffered_channel_threshold"] = 1;
1291+
// Prevent the reader from discarding the queued buffer (by switching back
1292+
// to the in-memory mode) once it has passed all data on.
1293+
config["file_buffered_channel_auto_truncate_file"] = false;
1294+
ensure(context.configure(config, errors));
1295+
1296+
startLoop();
1297+
1298+
feedCallerOwnedDataAndFreeItOncePassedOn("hello");
1299+
{
1300+
// The data was passed on before it was freed.
1301+
LOCK();
1302+
ensure_equals(log, "Data: hello\n");
1303+
}
1304+
ensure_equals(getChannelMode(), FileBufferedChannel::IN_FILE_MODE);
1305+
1306+
// Wait until the writer has moved the buffer to the file.
1307+
EVENTUALLY(5,
1308+
result = getChannelWriterState() == FileBufferedChannel::WS_INACTIVE
1309+
&& getChannelBytesBuffered() == 0;
1310+
);
1311+
}
12581312
}

0 commit comments

Comments
 (0)