[libcamera-devel] libcamera: base: log: Don't crash when logging after Logger destruction
diff mbox series

Message ID 20210827155841.17511-1-laurent.pinchart@ideasonboard.com
State Accepted
Commit c0ca2cbdc2dfcca50d7bc71a93629ca8e02b1682
Headers show
Series
  • [libcamera-devel] libcamera: base: log: Don't crash when logging after Logger destruction
Related show

Commit Message

Laurent Pinchart Aug. 27, 2021, 3:58 p.m. UTC
libcamera isn't supposed to log messages after the logger is destroyed,
as the global logger instance is destroyed after the main() function
returns, and the camera manager is supposed to have been stopped and
destroyed before that.

This rule is difficult to enforce in the V4L2 compat implementation, as
there is no location where we can destroy the camera manager manually
before the logger is destroyed. This results in a use-after-free
condition when the camera manager gets stopped during destruction.

Fix it by not trying to print log messages when the global logger
instance has been destroyed.

This is a bit of a hack, but hopefully not too bad. There could be race
conditions when using a CameraManager instance that is destroyed as part
of the destruction of global variables (like the V4L2 compat layer does,
it wraps CameraManager in a singleton V4L2CompatManager class, and
destroys it when V4L2CompatManager is destroyed) as the CameraManager
thread will still be running when the logger gets destroyed, but this
doesn't cause any regression as we destroy the logger without any
safeguard measure today anyway.

There are other options that could be considered. Forcing destruction of
the logger after the camera manager in the V4L2 compat layer is one of
them, but turned out to be difficult. For instance care would need to be
taken *not* to log any message in the mmap() wrapper if the fd doesn't
match a wrapped camera, as mmap() is called very early in the
initialization process, before libcamera and the logger get initialized.
The resulting implementation would likely be fairly complex.

Another option could be to wrap the logger with a shared pointer, and
keep a reference to it in CameraManager. That's more intrusive, and it's
not clear if it would be worth it.

Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
---
 src/libcamera/base/log.cpp | 18 ++++++++++++++++--
 1 file changed, 16 insertions(+), 2 deletions(-)

Comments

Paul Elder Sept. 1, 2021, 9:57 a.m. UTC | #1
Hi Laurent,

On Fri, Aug 27, 2021 at 06:58:41PM +0300, Laurent Pinchart wrote:
> libcamera isn't supposed to log messages after the logger is destroyed,
> as the global logger instance is destroyed after the main() function
> returns, and the camera manager is supposed to have been stopped and
> destroyed before that.
> 
> This rule is difficult to enforce in the V4L2 compat implementation, as
> there is no location where we can destroy the camera manager manually
> before the logger is destroyed. This results in a use-after-free
> condition when the camera manager gets stopped during destruction.
> 
> Fix it by not trying to print log messages when the global logger
> instance has been destroyed.
> 
> This is a bit of a hack, but hopefully not too bad. There could be race
> conditions when using a CameraManager instance that is destroyed as part
> of the destruction of global variables (like the V4L2 compat layer does,
> it wraps CameraManager in a singleton V4L2CompatManager class, and
> destroys it when V4L2CompatManager is destroyed) as the CameraManager
> thread will still be running when the logger gets destroyed, but this
> doesn't cause any regression as we destroy the logger without any
> safeguard measure today anyway.

This looks like a good protection in general.

> 
> There are other options that could be considered. Forcing destruction of
> the logger after the camera manager in the V4L2 compat layer is one of
> them, but turned out to be difficult. For instance care would need to be
> taken *not* to log any message in the mmap() wrapper if the fd doesn't
> match a wrapped camera, as mmap() is called very early in the
> initialization process, before libcamera and the logger get initialized.
> The resulting implementation would likely be fairly complex.
> 
> Another option could be to wrap the logger with a shared pointer, and
> keep a reference to it in CameraManager. That's more intrusive, and it's
> not clear if it would be worth it.
> 
> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
> ---
>  src/libcamera/base/log.cpp | 18 ++++++++++++++++--
>  1 file changed, 16 insertions(+), 2 deletions(-)
> 
> diff --git a/src/libcamera/base/log.cpp b/src/libcamera/base/log.cpp
> index 073b7c34b75e..a3e3f9ea2712 100644
> --- a/src/libcamera/base/log.cpp
> +++ b/src/libcamera/base/log.cpp
> @@ -270,12 +270,16 @@ private:
>  	friend LogCategory;
>  	void registerCategory(LogCategory *category);
>  
> +	static bool destroyed_;
> +
>  	std::unordered_set<LogCategory *> categories_;
>  	std::list<std::pair<std::string, LogSeverity>> levels_;
>  
>  	std::shared_ptr<LogOutput> output_;
>  };
>  
> +bool Logger::destroyed_ = false;

What's the merit of putting this here instead of in the constructor?


Paul

> +
>  /**
>   * \enum LoggingTarget
>   * \brief Log destination type
> @@ -372,6 +376,8 @@ void logSetLevel(const char *category, const char *level)
>  
>  Logger::~Logger()
>  {
> +	destroyed_ = true;
> +
>  	for (LogCategory *category : categories_)
>  		delete category;
>  }
> @@ -387,6 +393,10 @@ Logger::~Logger()
>  Logger *Logger::instance()
>  {
>  	static Logger instance;
> +
> +	if (destroyed_)
> +		return nullptr;
> +
>  	return &instance;
>  }
>  
> @@ -808,13 +818,17 @@ LogMessage::~LogMessage()
>  	if (severity_ == LogInvalid)
>  		return;
>  
> +	Logger *logger = Logger::instance();
> +	if (!logger)
> +		return;
> +
>  	msgStream_ << std::endl;
>  
>  	if (severity_ >= category_.severity())
> -		Logger::instance()->write(*this);
> +		logger->write(*this);
>  
>  	if (severity_ == LogSeverity::LogFatal) {
> -		Logger::instance()->backtrace();
> +		logger->backtrace();
>  		std::abort();
>  	}
>  }
> -- 
> Regards,
> 
> Laurent Pinchart
>
Laurent Pinchart Sept. 1, 2021, 10 a.m. UTC | #2
Hi Paul,

On Wed, Sep 01, 2021 at 06:57:36PM +0900, paul.elder@ideasonboard.com wrote:
> On Fri, Aug 27, 2021 at 06:58:41PM +0300, Laurent Pinchart wrote:
> > libcamera isn't supposed to log messages after the logger is destroyed,
> > as the global logger instance is destroyed after the main() function
> > returns, and the camera manager is supposed to have been stopped and
> > destroyed before that.
> > 
> > This rule is difficult to enforce in the V4L2 compat implementation, as
> > there is no location where we can destroy the camera manager manually
> > before the logger is destroyed. This results in a use-after-free
> > condition when the camera manager gets stopped during destruction.
> > 
> > Fix it by not trying to print log messages when the global logger
> > instance has been destroyed.
> > 
> > This is a bit of a hack, but hopefully not too bad. There could be race
> > conditions when using a CameraManager instance that is destroyed as part
> > of the destruction of global variables (like the V4L2 compat layer does,
> > it wraps CameraManager in a singleton V4L2CompatManager class, and
> > destroys it when V4L2CompatManager is destroyed) as the CameraManager
> > thread will still be running when the logger gets destroyed, but this
> > doesn't cause any regression as we destroy the logger without any
> > safeguard measure today anyway.
> 
> This looks like a good protection in general.
> 
> > There are other options that could be considered. Forcing destruction of
> > the logger after the camera manager in the V4L2 compat layer is one of
> > them, but turned out to be difficult. For instance care would need to be
> > taken *not* to log any message in the mmap() wrapper if the fd doesn't
> > match a wrapped camera, as mmap() is called very early in the
> > initialization process, before libcamera and the logger get initialized.
> > The resulting implementation would likely be fairly complex.
> > 
> > Another option could be to wrap the logger with a shared pointer, and
> > keep a reference to it in CameraManager. That's more intrusive, and it's
> > not clear if it would be worth it.
> > 
> > Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
> > ---
> >  src/libcamera/base/log.cpp | 18 ++++++++++++++++--
> >  1 file changed, 16 insertions(+), 2 deletions(-)
> > 
> > diff --git a/src/libcamera/base/log.cpp b/src/libcamera/base/log.cpp
> > index 073b7c34b75e..a3e3f9ea2712 100644
> > --- a/src/libcamera/base/log.cpp
> > +++ b/src/libcamera/base/log.cpp
> > @@ -270,12 +270,16 @@ private:
> >  	friend LogCategory;
> >  	void registerCategory(LogCategory *category);
> >  
> > +	static bool destroyed_;
> > +
> >  	std::unordered_set<LogCategory *> categories_;
> >  	std::list<std::pair<std::string, LogSeverity>> levels_;
> >  
> >  	std::shared_ptr<LogOutput> output_;
> >  };
> >  
> > +bool Logger::destroyed_ = false;
> 
> What's the merit of putting this here instead of in the constructor?

It's a static class member, so it's effectively a global variable, not a
per-instance member variable.

> > +
> >  /**
> >   * \enum LoggingTarget
> >   * \brief Log destination type
> > @@ -372,6 +376,8 @@ void logSetLevel(const char *category, const char *level)
> >  
> >  Logger::~Logger()
> >  {
> > +	destroyed_ = true;
> > +
> >  	for (LogCategory *category : categories_)
> >  		delete category;
> >  }
> > @@ -387,6 +393,10 @@ Logger::~Logger()
> >  Logger *Logger::instance()
> >  {
> >  	static Logger instance;
> > +
> > +	if (destroyed_)
> > +		return nullptr;
> > +
> >  	return &instance;
> >  }
> >  
> > @@ -808,13 +818,17 @@ LogMessage::~LogMessage()
> >  	if (severity_ == LogInvalid)
> >  		return;
> >  
> > +	Logger *logger = Logger::instance();
> > +	if (!logger)
> > +		return;
> > +
> >  	msgStream_ << std::endl;
> >  
> >  	if (severity_ >= category_.severity())
> > -		Logger::instance()->write(*this);
> > +		logger->write(*this);
> >  
> >  	if (severity_ == LogSeverity::LogFatal) {
> > -		Logger::instance()->backtrace();
> > +		logger->backtrace();
> >  		std::abort();
> >  	}
> >  }
Paul Elder Sept. 1, 2021, 10:02 a.m. UTC | #3
Hi Laurent,

On Wed, Sep 01, 2021 at 01:00:46PM +0300, Laurent Pinchart wrote:
> Hi Paul,
> 
> On Wed, Sep 01, 2021 at 06:57:36PM +0900, paul.elder@ideasonboard.com wrote:
> > On Fri, Aug 27, 2021 at 06:58:41PM +0300, Laurent Pinchart wrote:
> > > libcamera isn't supposed to log messages after the logger is destroyed,
> > > as the global logger instance is destroyed after the main() function
> > > returns, and the camera manager is supposed to have been stopped and
> > > destroyed before that.
> > > 
> > > This rule is difficult to enforce in the V4L2 compat implementation, as
> > > there is no location where we can destroy the camera manager manually
> > > before the logger is destroyed. This results in a use-after-free
> > > condition when the camera manager gets stopped during destruction.
> > > 
> > > Fix it by not trying to print log messages when the global logger
> > > instance has been destroyed.
> > > 
> > > This is a bit of a hack, but hopefully not too bad. There could be race
> > > conditions when using a CameraManager instance that is destroyed as part
> > > of the destruction of global variables (like the V4L2 compat layer does,
> > > it wraps CameraManager in a singleton V4L2CompatManager class, and
> > > destroys it when V4L2CompatManager is destroyed) as the CameraManager
> > > thread will still be running when the logger gets destroyed, but this
> > > doesn't cause any regression as we destroy the logger without any
> > > safeguard measure today anyway.
> > 
> > This looks like a good protection in general.
> > 
> > > There are other options that could be considered. Forcing destruction of
> > > the logger after the camera manager in the V4L2 compat layer is one of
> > > them, but turned out to be difficult. For instance care would need to be
> > > taken *not* to log any message in the mmap() wrapper if the fd doesn't
> > > match a wrapped camera, as mmap() is called very early in the
> > > initialization process, before libcamera and the logger get initialized.
> > > The resulting implementation would likely be fairly complex.
> > > 
> > > Another option could be to wrap the logger with a shared pointer, and
> > > keep a reference to it in CameraManager. That's more intrusive, and it's
> > > not clear if it would be worth it.
> > > 
> > > Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
> > > ---
> > >  src/libcamera/base/log.cpp | 18 ++++++++++++++++--
> > >  1 file changed, 16 insertions(+), 2 deletions(-)
> > > 
> > > diff --git a/src/libcamera/base/log.cpp b/src/libcamera/base/log.cpp
> > > index 073b7c34b75e..a3e3f9ea2712 100644
> > > --- a/src/libcamera/base/log.cpp
> > > +++ b/src/libcamera/base/log.cpp
> > > @@ -270,12 +270,16 @@ private:
> > >  	friend LogCategory;
> > >  	void registerCategory(LogCategory *category);
> > >  
> > > +	static bool destroyed_;
> > > +
> > >  	std::unordered_set<LogCategory *> categories_;
> > >  	std::list<std::pair<std::string, LogSeverity>> levels_;
> > >  
> > >  	std::shared_ptr<LogOutput> output_;
> > >  };
> > >  
> > > +bool Logger::destroyed_ = false;
> > 
> > What's the merit of putting this here instead of in the constructor?
> 
> It's a static class member, so it's effectively a global variable, not a
> per-instance member variable.

Oh right, I see. Thanks for the clarification.

Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>

> 
> > > +
> > >  /**
> > >   * \enum LoggingTarget
> > >   * \brief Log destination type
> > > @@ -372,6 +376,8 @@ void logSetLevel(const char *category, const char *level)
> > >  
> > >  Logger::~Logger()
> > >  {
> > > +	destroyed_ = true;
> > > +
> > >  	for (LogCategory *category : categories_)
> > >  		delete category;
> > >  }
> > > @@ -387,6 +393,10 @@ Logger::~Logger()
> > >  Logger *Logger::instance()
> > >  {
> > >  	static Logger instance;
> > > +
> > > +	if (destroyed_)
> > > +		return nullptr;
> > > +
> > >  	return &instance;
> > >  }
> > >  
> > > @@ -808,13 +818,17 @@ LogMessage::~LogMessage()
> > >  	if (severity_ == LogInvalid)
> > >  		return;
> > >  
> > > +	Logger *logger = Logger::instance();
> > > +	if (!logger)
> > > +		return;
> > > +
> > >  	msgStream_ << std::endl;
> > >  
> > >  	if (severity_ >= category_.severity())
> > > -		Logger::instance()->write(*this);
> > > +		logger->write(*this);
> > >  
> > >  	if (severity_ == LogSeverity::LogFatal) {
> > > -		Logger::instance()->backtrace();
> > > +		logger->backtrace();
> > >  		std::abort();
> > >  	}
> > >  }
> 
> -- 
> Regards,
> 
> Laurent Pinchart

Patch
diff mbox series

diff --git a/src/libcamera/base/log.cpp b/src/libcamera/base/log.cpp
index 073b7c34b75e..a3e3f9ea2712 100644
--- a/src/libcamera/base/log.cpp
+++ b/src/libcamera/base/log.cpp
@@ -270,12 +270,16 @@  private:
 	friend LogCategory;
 	void registerCategory(LogCategory *category);
 
+	static bool destroyed_;
+
 	std::unordered_set<LogCategory *> categories_;
 	std::list<std::pair<std::string, LogSeverity>> levels_;
 
 	std::shared_ptr<LogOutput> output_;
 };
 
+bool Logger::destroyed_ = false;
+
 /**
  * \enum LoggingTarget
  * \brief Log destination type
@@ -372,6 +376,8 @@  void logSetLevel(const char *category, const char *level)
 
 Logger::~Logger()
 {
+	destroyed_ = true;
+
 	for (LogCategory *category : categories_)
 		delete category;
 }
@@ -387,6 +393,10 @@  Logger::~Logger()
 Logger *Logger::instance()
 {
 	static Logger instance;
+
+	if (destroyed_)
+		return nullptr;
+
 	return &instance;
 }
 
@@ -808,13 +818,17 @@  LogMessage::~LogMessage()
 	if (severity_ == LogInvalid)
 		return;
 
+	Logger *logger = Logger::instance();
+	if (!logger)
+		return;
+
 	msgStream_ << std::endl;
 
 	if (severity_ >= category_.severity())
-		Logger::instance()->write(*this);
+		logger->write(*this);
 
 	if (severity_ == LogSeverity::LogFatal) {
-		Logger::instance()->backtrace();
+		logger->backtrace();
 		std::abort();
 	}
 }