Skip to content

bug: ImageBuf destructor crashes application if OIIO::print fails #5063

@adskWangl

Description

@adskWangl

Description:
I encountered a situation where applications using OpenImageIO are terminated abruptly. This happens in the ImageBuf destructor when it attempts to print uncaught errors: imagebuf.cpp#L574-L582

Root Cause:

  1. Throwing in Destructor: OIIO::print relies on fmt. The bundled or external fmt implementation (specifically fwrite_fully in format-inl.h) throws a system_error if std::fwrite writes fewer bytes than expected.
    // include/OpenImageIO/detail/fmt/format-inl.h
    inline void fwrite_fully(const void* ptr, size_t count, FILE* stream) {
      size_t written = std::fwrite(ptr, 1, count, stream);
      if (written < count)
        FMT_THROW(system_error(errno, FMT_STRING("cannot write to file")));
    }
  2. Terminate: Since ImageBufImpl::~ImageBufImpl is a destructor, if this exception is thrown (especially during stack unwinding from another exception), std::terminate is called, killing the entire application.
    // include/OpenImageIO/detail/fmt/format.h
    #ifndef FMT_THROW
    #  if FMT_EXCEPTIONS
    #    if FMT_MSC_VERSION || defined(__NVCC__)
    FMT_BEGIN_NAMESPACE
    namespace detail {
    template <typename Exception> inline void do_throw(const Exception& x) {
    // Silence unreachable code warnings in MSVC and NVCC because these
    // are nearly impossible to fix in a generic code.
    volatile bool b = true;
    if (b) throw x;
    }
    }  // namespace detail
    FMT_END_NAMESPACE
    #      define FMT_THROW(x) detail::do_throw(x)
    #    else
    #      define FMT_THROW(x) throw x
    #    endif
    #  else
    #    define FMT_THROW(x) \
        ::fmt::detail::assert_fail(__FILE__, __LINE__, (x).what())
    #  endif
    #endif

Issues for Discussion:

  1. Error Handling Mix-up: I think the intention of the code in the destructor is to help devs debug and correct logic errors. It's reasonable to alert if there are unhandled errors. However, a failure in this diagnostic printing mechanism shouldn't escalate to an application crash.

  2. FMT_THROW Configuration: There seems to be a mix of identifying errors via asserts and exceptions (see the code block I attached above). In addition, the usage of FMT_THROW implies exceptions are expected, but OIIO code calling it from noexcept contexts (like destructors) is dangerous.

  3. Reliability of std::fwrite check: Using the return value of std::fwrite to strictly enforce "all bytes written or die" could be too aggressive, especially for logging/diagnostic output.

    • std::fwrite may write fewer items than requested if the underlying stream buffer is full (e.g., specific pipe buffer limits when redirecting stdout/stderr) or if the consumer of the pipe is slow or has disconnected.
    • A partial write is not necessarily a fatal error that warrants terminating the process, especially when simply trying to log a warning during destruction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions