Skip to content

Cryptic/garbled RuntimeError from ffi at boot under systemd's MemoryDenyWriteExecute=yes #439

Description

@paulschreiber

Describe the bug

On an Ubuntu server running Apache + Phusion Passenger where the apache2.service systemd unit has MemoryDenyWriteExecute=yes set, requiring ruby-vips causes the whole app to fail to boot with an unreadable, garbled RuntimeError raised from deep inside ffi, at the very first attach_function call in vips.rb:

؀}?u (RuntimeError)
  /path/to/railsapp/shared/bundle/ruby/3.3.0/gems/ffi-1.17.4-x86_64-linux-gnu/lib/ffi/function.rb:88:in `attach'
  /path/to/railsapp/shared/bundle/ruby/3.3.0/gems/ffi-1.17.4-x86_64-linux-gnu/lib/ffi/function.rb:88:in `attach'
  /path/to/railsapp/shared/bundle/ruby/3.3.0/gems/ffi-1.17.4-x86_64-linux-gnu/lib/ffi/library.rb:218:in `attach_function'
  /path/to/railsapp/shared/bundle/ruby/3.3.0/gems/ruby-vips-2.3.0/lib/vips.rb:51:in `<module:Vips>'
  /path/to/railsapp/shared/bundle/ruby/3.3.0/gems/ruby-vips-2.3.0/lib/vips.rb:45:in `<main>'

The exception message itself is non-printable/garbled bytes.

Root cause

vips.rb:51 does:

begin
  attach_function :g_malloc, [:size_t], :pointer
  @@is_unified = true
rescue FFI::NotFoundError
  @@is_unified = false
end                                                                                                                  

Under MemoryDenyWriteExecute=yes, the the underlying mprotect(..., PROT_READ|PROT_EXEC) call that libffi makes to finalize a function-call trampoline fails with EACCES.

This was confirmed via strace -f, which surfaces as a raw RuntimeError rather than FFI::NotFoundError — so the rescue here never catches it, and the exception propagates all the way up and crashes the process at require-time.

MemoryDenyWriteExecute is a systemd hardening option that's explicitly documented as incompatible with anything using JIT/trampoline-stylecode generation, which is exactly what libffi does here. This isn't ruby-vips- or libffi-specific — Python's cffi hits the identical failure mode under the same systemd setting (openssl #873, rpy2 #755). I couldn't find this written up anywhere for Ruby/ruby-vips/Rails specifcally, despite ruby-vips now being a default Rails 8.1 ActiveStorage dependency.

Environment

  • ruby-vips 2.3.0, ffi 1.17.4 (x86_64-linux-gnu)
  • Ruby 3.3.8, Rails 8.1 (config.load_defaults 8.1 implicitly selects :vips as variant_processor)
  • Ubuntu 26.10 (glibc 2.43), Apache 2.4.66 + Phusion Passenger 6.1.1

Workaround

Disabling the systemd hardening option for the service resolves it:

sudo systemctl edit apache2

add this config:

[Service]
MemoryDenyWriteExecute=no

Restart Apache

sudo systemctl daemon-reload && sudo systemctl restart apache2

Suggestions

  • Gracefully handle the error (rescue RuntimeError, or some other method)
  • Improve the error message and ensure it is logged correctly.
  • Add a note about this README/troubleshooting docs. Given ruby-vips is now pulled in by default for any Rails 8.1 app using ActiveStorage, and the failure mode here is close to undiagnosable from the error message alone, a pointer to "checkMemoryDenyWriteExecute if you're on a hardened systemd host" could save others a lot of time.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions