[libcamera-devel,3/3] apps: cam: kms_sink: Introduce a requests tracking queue
diff mbox series

Message ID 20230423203931.108022-4-umang.jain@ideasonboard.com
State New
Headers show
Series
  • apps: cam: kms: Introduce requests tracking queue
Related show

Commit Message

Umang Jain April 23, 2023, 8:39 p.m. UTC
Currently the queue depth tracking DRM completed requests is
effectively 2, via queued_ and pending_ class members in KMSSink.
This patch introduces a queue which can track more requests thus giving
a higher queue depth.

The reason to introduce a higher queue depth is to avoid use-after-free
on KMSSink::stop() in cases where KMSSink class is frequently operated
under: start() -> configure() -> stop() cycles. As soon as the
KMSSink::stop() is called, it used to free the queued_ and pending_
requests, but a DRM request can still complete asynchronously (and
after the queued_ and pending_ are freed). This led to use-after-free
segfault in Device::pageFlipComplete() while emitting the
`requestComplete` signal on a (already freed) request.

In the design introduced in this patch, the requests already in the queue
are marked as 'expired' and not freed in KMSSink::stop(). This prevents
the use-after-free segfault in Device::pageFlipComplete(). The expired
requests are dropped from the queue when new requests come into the
queue and gets completed in the KMSSink::requestComplete() handler.

Signed-off-by: Umang Jain <umang.jain@ideasonboard.com>
---
 src/apps/cam/kms_sink.cpp | 73 +++++++++++++++++++++------------------
 src/apps/cam/kms_sink.h   | 11 +++---
 2 files changed, 47 insertions(+), 37 deletions(-)

Comments

Laurent Pinchart April 24, 2023, 1:22 a.m. UTC | #1
Hi Umang,

Thank you for the patch.

On Mon, Apr 24, 2023 at 02:09:31AM +0530, Umang Jain via libcamera-devel wrote:
> Currently the queue depth tracking DRM completed requests is
> effectively 2, via queued_ and pending_ class members in KMSSink.
> This patch introduces a queue which can track more requests thus giving
> a higher queue depth.
> 
> The reason to introduce a higher queue depth is to avoid use-after-free
> on KMSSink::stop() in cases where KMSSink class is frequently operated
> under: start() -> configure() -> stop() cycles. As soon as the
> KMSSink::stop() is called, it used to free the queued_ and pending_
> requests, but a DRM request can still complete asynchronously (and
> after the queued_ and pending_ are freed). This led to use-after-free
> segfault in Device::pageFlipComplete() while emitting the
> `requestComplete` signal on a (already freed) request.
> 
> In the design introduced in this patch, the requests already in the queue
> are marked as 'expired' and not freed in KMSSink::stop(). This prevents
> the use-after-free segfault in Device::pageFlipComplete(). The expired
> requests are dropped from the queue when new requests come into the
> queue and gets completed in the KMSSink::requestComplete() handler.
> 
> Signed-off-by: Umang Jain <umang.jain@ideasonboard.com>
> ---
>  src/apps/cam/kms_sink.cpp | 73 +++++++++++++++++++++------------------
>  src/apps/cam/kms_sink.h   | 11 +++---
>  2 files changed, 47 insertions(+), 37 deletions(-)
> 
> diff --git a/src/apps/cam/kms_sink.cpp b/src/apps/cam/kms_sink.cpp
> index 2aefec06..8305e6de 100644
> --- a/src/apps/cam/kms_sink.cpp
> +++ b/src/apps/cam/kms_sink.cpp
> @@ -24,7 +24,8 @@
>  #include "drm.h"
>  
>  KMSSink::KMSSink(const std::string &connectorName)
> -	: connector_(nullptr), crtc_(nullptr), plane_(nullptr), mode_(nullptr)
> +	: connector_(nullptr), crtc_(nullptr), plane_(nullptr), mode_(nullptr),
> +	  firstFrame_(false)
>  {
>  	int ret = dev_.init();
>  	if (ret < 0)
> @@ -327,6 +328,8 @@ int KMSSink::start()
>  
>  	dev_.requestComplete.connect(this, &KMSSink::requestComplete);
>  
> +	firstFrame_ = true;
> +
>  	return 0;
>  }
>  
> @@ -334,6 +337,13 @@ int KMSSink::stop()
>  {
>  	dev_.requestComplete.disconnect();
>  
> +	{
> +		std::lock_guard<std::mutex> lock(lock_);
> +		/* Expire all the DRM requests in the queue */
> +		for (std::unique_ptr<Request> &req : requests_)
> +			req->expired_ = true;
> +	}
> +
>  	/* Display pipeline. */
>  	DRM::AtomicRequest request(&dev_);
>  
> @@ -352,9 +362,6 @@ int KMSSink::stop()
>  	}
>  
>  	/* Free all buffers. */
> -	pending_.reset();
> -	queued_.reset();
> -	active_.reset();
>  	buffers_.clear();
>  
>  	return FrameSink::stop();
> @@ -450,13 +457,6 @@ bool KMSSink::setupComposition(DRM::FrameBuffer *drmBuffer)
>  
>  bool KMSSink::processRequest(libcamera::Request *camRequest)
>  {
> -	/*
> -	 * Perform a very crude rate adaptation by simply dropping the request
> -	 * if the display queue is full.
> -	 */
> -	if (pending_)
> -		return true;

The KMS API doesn't support queue depth larger than 1. This is why the
KMSSink doesn't use a larger queue. I don't recall what happens if you
try to queue more commits than KMS can handle, whether the atomic commit
ioctl will fail, or if it will wait synchronously for the previous
commit to complete. In either case, that's not right. KMSSink needs to
ensure that it doesn't overflow the (small) KMS commit queue.

> -
>  	libcamera::FrameBuffer *buffer = camRequest->buffers().begin()->second;
>  	auto iter = buffers_.find(buffer);
>  	if (iter == buffers_.end())
> @@ -469,7 +469,7 @@ bool KMSSink::processRequest(libcamera::Request *camRequest)
>  		std::make_unique<DRM::AtomicRequest>(&dev_);
>  	drmRequest->addProperty(plane_, "FB_ID", drmBuffer->id());
>  
> -	if (!active_ && !queued_) {
> +	if (firstFrame_) {
>  		/* Enable the display pipeline on the first frame. */
>  		if (!setupComposition(drmBuffer)) {
>  			std::cerr << "Failed to setup composition" << std::endl;
> @@ -497,22 +497,22 @@ bool KMSSink::processRequest(libcamera::Request *camRequest)
>  			drmRequest->addProperty(plane_, "COLOR_RANGE", *colorRange_);
>  
>  		flags |= DRM::AtomicRequest::FlagAllowModeset;
> +		firstFrame_ = false;
>  	}
>  
> -	pending_ = std::make_unique<Request>(std::move(drmRequest), camRequest);
> +	std::unique_ptr<Request> pending =
> +		std::make_unique<Request>(std::move(drmRequest), camRequest);
>  
>  	std::lock_guard<std::mutex> lock(lock_);
>  
> -	if (!queued_) {
> -		int ret = pending_->drmRequest_->commit(flags);
> -		if (ret < 0) {
> -			std::cerr
> -				<< "Failed to commit atomic request: "
> -				<< strerror(-ret) << std::endl;
> -			/* \todo Implement error handling */
> -		}
> -
> -		queued_ = std::move(pending_);
> +	int ret = pending->drmRequest_->commit(flags);
> +	if (ret < 0) {
> +		std::cerr
> +			<< "Failed to commit atomic request: "
> +			<< strerror(-ret) << std::endl;
> +		/* \todo Implement error handling */
> +	} else {
> +		requests_.push_back(std::move(pending));
>  	}
>  
>  	return false;
> @@ -522,18 +522,25 @@ void KMSSink::requestComplete([[maybe_unused]] DRM::AtomicRequest *request)
>  {
>  	std::lock_guard<std::mutex> lock(lock_);
>  
> -	assert(queued_ && queued_->drmRequest_.get() == request);
> +	std::unique_ptr<Request> &headReq = requests_.front();
>  
> -	/* Complete the active request, if any. */
> -	if (active_)
> -		requestProcessed.emit(active_->camRequest_);
> +	assert(headReq->drmRequest_.get() == request);
>  
> -	/* The queued request becomes active. */
> -	active_ = std::move(queued_);
> +	if (!headReq->expired_) {
> +		requestProcessed.emit(headReq->camRequest_);
> +		requests_.pop_front();
> +	} else {
> +		/* Remove candidates which are expired */
> +		while (requests_.size() > 0) {
> +			if (requests_.front()->expired_)
> +				requests_.pop_front();
> +			else
> +				break;
> +		}
>  
> -	/* Queue the pending request, if any. */
> -	if (pending_) {
> -		pending_->drmRequest_->commit(DRM::AtomicRequest::FlagAsync);
> -		queued_ = std::move(pending_);
> +		return;
>  	}
> +
> +	if (requests_.size())
> +		requests_.front()->drmRequest_->commit(DRM::AtomicRequest::FlagAsync);
>  }
> diff --git a/src/apps/cam/kms_sink.h b/src/apps/cam/kms_sink.h
> index e2c618a1..a6b418aa 100644
> --- a/src/apps/cam/kms_sink.h
> +++ b/src/apps/cam/kms_sink.h
> @@ -7,6 +7,7 @@
>  
>  #pragma once
>  
> +#include <deque>
>  #include <list>
>  #include <memory>
>  #include <mutex>
> @@ -41,12 +42,14 @@ private:
>  	public:
>  		Request(std::unique_ptr<DRM::AtomicRequest> drmRequest,
>  			libcamera::Request *camRequest)
> -			: drmRequest_(std::move(drmRequest)), camRequest_(camRequest)
> +			: drmRequest_(std::move(drmRequest)), camRequest_(camRequest),
> +			  expired_(false)
>  		{
>  		}
>  
>  		std::unique_ptr<DRM::AtomicRequest> drmRequest_;
>  		libcamera::Request *camRequest_;
> +		bool expired_;
>  	};
>  
>  	int selectPipeline(const libcamera::PixelFormat &format);
> @@ -76,8 +79,8 @@ private:
>  
>  	std::map<libcamera::FrameBuffer *, std::unique_ptr<DRM::FrameBuffer>> buffers_;
>  
> +	bool firstFrame_;
> +
>  	std::mutex lock_;
> -	std::unique_ptr<Request> pending_;
> -	std::unique_ptr<Request> queued_;
> -	std::unique_ptr<Request> active_;
> +	std::deque<std::unique_ptr<Request>> requests_;
>  };

Patch
diff mbox series

diff --git a/src/apps/cam/kms_sink.cpp b/src/apps/cam/kms_sink.cpp
index 2aefec06..8305e6de 100644
--- a/src/apps/cam/kms_sink.cpp
+++ b/src/apps/cam/kms_sink.cpp
@@ -24,7 +24,8 @@ 
 #include "drm.h"
 
 KMSSink::KMSSink(const std::string &connectorName)
-	: connector_(nullptr), crtc_(nullptr), plane_(nullptr), mode_(nullptr)
+	: connector_(nullptr), crtc_(nullptr), plane_(nullptr), mode_(nullptr),
+	  firstFrame_(false)
 {
 	int ret = dev_.init();
 	if (ret < 0)
@@ -327,6 +328,8 @@  int KMSSink::start()
 
 	dev_.requestComplete.connect(this, &KMSSink::requestComplete);
 
+	firstFrame_ = true;
+
 	return 0;
 }
 
@@ -334,6 +337,13 @@  int KMSSink::stop()
 {
 	dev_.requestComplete.disconnect();
 
+	{
+		std::lock_guard<std::mutex> lock(lock_);
+		/* Expire all the DRM requests in the queue */
+		for (std::unique_ptr<Request> &req : requests_)
+			req->expired_ = true;
+	}
+
 	/* Display pipeline. */
 	DRM::AtomicRequest request(&dev_);
 
@@ -352,9 +362,6 @@  int KMSSink::stop()
 	}
 
 	/* Free all buffers. */
-	pending_.reset();
-	queued_.reset();
-	active_.reset();
 	buffers_.clear();
 
 	return FrameSink::stop();
@@ -450,13 +457,6 @@  bool KMSSink::setupComposition(DRM::FrameBuffer *drmBuffer)
 
 bool KMSSink::processRequest(libcamera::Request *camRequest)
 {
-	/*
-	 * Perform a very crude rate adaptation by simply dropping the request
-	 * if the display queue is full.
-	 */
-	if (pending_)
-		return true;
-
 	libcamera::FrameBuffer *buffer = camRequest->buffers().begin()->second;
 	auto iter = buffers_.find(buffer);
 	if (iter == buffers_.end())
@@ -469,7 +469,7 @@  bool KMSSink::processRequest(libcamera::Request *camRequest)
 		std::make_unique<DRM::AtomicRequest>(&dev_);
 	drmRequest->addProperty(plane_, "FB_ID", drmBuffer->id());
 
-	if (!active_ && !queued_) {
+	if (firstFrame_) {
 		/* Enable the display pipeline on the first frame. */
 		if (!setupComposition(drmBuffer)) {
 			std::cerr << "Failed to setup composition" << std::endl;
@@ -497,22 +497,22 @@  bool KMSSink::processRequest(libcamera::Request *camRequest)
 			drmRequest->addProperty(plane_, "COLOR_RANGE", *colorRange_);
 
 		flags |= DRM::AtomicRequest::FlagAllowModeset;
+		firstFrame_ = false;
 	}
 
-	pending_ = std::make_unique<Request>(std::move(drmRequest), camRequest);
+	std::unique_ptr<Request> pending =
+		std::make_unique<Request>(std::move(drmRequest), camRequest);
 
 	std::lock_guard<std::mutex> lock(lock_);
 
-	if (!queued_) {
-		int ret = pending_->drmRequest_->commit(flags);
-		if (ret < 0) {
-			std::cerr
-				<< "Failed to commit atomic request: "
-				<< strerror(-ret) << std::endl;
-			/* \todo Implement error handling */
-		}
-
-		queued_ = std::move(pending_);
+	int ret = pending->drmRequest_->commit(flags);
+	if (ret < 0) {
+		std::cerr
+			<< "Failed to commit atomic request: "
+			<< strerror(-ret) << std::endl;
+		/* \todo Implement error handling */
+	} else {
+		requests_.push_back(std::move(pending));
 	}
 
 	return false;
@@ -522,18 +522,25 @@  void KMSSink::requestComplete([[maybe_unused]] DRM::AtomicRequest *request)
 {
 	std::lock_guard<std::mutex> lock(lock_);
 
-	assert(queued_ && queued_->drmRequest_.get() == request);
+	std::unique_ptr<Request> &headReq = requests_.front();
 
-	/* Complete the active request, if any. */
-	if (active_)
-		requestProcessed.emit(active_->camRequest_);
+	assert(headReq->drmRequest_.get() == request);
 
-	/* The queued request becomes active. */
-	active_ = std::move(queued_);
+	if (!headReq->expired_) {
+		requestProcessed.emit(headReq->camRequest_);
+		requests_.pop_front();
+	} else {
+		/* Remove candidates which are expired */
+		while (requests_.size() > 0) {
+			if (requests_.front()->expired_)
+				requests_.pop_front();
+			else
+				break;
+		}
 
-	/* Queue the pending request, if any. */
-	if (pending_) {
-		pending_->drmRequest_->commit(DRM::AtomicRequest::FlagAsync);
-		queued_ = std::move(pending_);
+		return;
 	}
+
+	if (requests_.size())
+		requests_.front()->drmRequest_->commit(DRM::AtomicRequest::FlagAsync);
 }
diff --git a/src/apps/cam/kms_sink.h b/src/apps/cam/kms_sink.h
index e2c618a1..a6b418aa 100644
--- a/src/apps/cam/kms_sink.h
+++ b/src/apps/cam/kms_sink.h
@@ -7,6 +7,7 @@ 
 
 #pragma once
 
+#include <deque>
 #include <list>
 #include <memory>
 #include <mutex>
@@ -41,12 +42,14 @@  private:
 	public:
 		Request(std::unique_ptr<DRM::AtomicRequest> drmRequest,
 			libcamera::Request *camRequest)
-			: drmRequest_(std::move(drmRequest)), camRequest_(camRequest)
+			: drmRequest_(std::move(drmRequest)), camRequest_(camRequest),
+			  expired_(false)
 		{
 		}
 
 		std::unique_ptr<DRM::AtomicRequest> drmRequest_;
 		libcamera::Request *camRequest_;
+		bool expired_;
 	};
 
 	int selectPipeline(const libcamera::PixelFormat &format);
@@ -76,8 +79,8 @@  private:
 
 	std::map<libcamera::FrameBuffer *, std::unique_ptr<DRM::FrameBuffer>> buffers_;
 
+	bool firstFrame_;
+
 	std::mutex lock_;
-	std::unique_ptr<Request> pending_;
-	std::unique_ptr<Request> queued_;
-	std::unique_ptr<Request> active_;
+	std::deque<std::unique_ptr<Request>> requests_;
 };