Coping with Plug-in load/scan failures (e.g. EXC_BAD_ACCESS)

Hi JUCE devs (@ed95 ?)

I’ve been dealing with problems scanning some ill-behaved plug-ins.

One example would be Harvest on macOS, which crashes with EXC_BAD_ACCESS (I’ve raised this with their devs, and would hope they can fix this soon).

It turns out that I can defend against quite easily using something like the following: GitHub - dhatbj/SignalRecovery: A C library that keep your program from crashing when illegal access occurs.

Example:

AudioUnitPluginFormat::createPluginInstance(...)

        signal_catch_init();
      
        AudioComponentInstance audioUnit;
        int err = noErr;
        signal_try(label) {
            err = AudioComponentInstanceNew(auComponent, &audioUnit);
        } signal_catch(label) {
            // Something horrible like EXC_BAD_ACCESS has just happened
            err = -1;
        }
        signal_end(label);

In the debugger, lldb will just sit there on EXC_BAD_ACCESS, but if you don’t run the app through the debugger, the app will recover from EXC_BAD_ACCESS and can report the plug-in as having failed scanning.

Is this something you’ve considered adding to JUCE?

Best wishes,

Pete

The usual approach to this problem is to scan plug-ins in a separate process. We added this to the AudioPlug-in host fairly recently, so that’s an example of one way of doing this.

2 Likes

@t0m, this is awesome!
Any chance to have that class exposed in the interface of JUCE instead of being an implementation-only class?

To tidy it up enough to be part of the public API would be quite a lot of effort. We can look at doing it, but we’ve also got a lot of more high priority work to get through first.

1 Like

Hi @t0m a great idea; but AFAIK, it isn’t possible to do this for sandboxed apps from the Apple App Stores (iOS, macOS). It is certainly an option for Windows, however!

On iOS/macOS, I think the signal_catch_init()… idea is actually quite a good compromise idea to consider.

I know none of this is easy!

Best wishes, Pete

It’s not easy, but it’s also not a good motivation to use something like you’re proposing.

If the EXC_BAD_ACCESS from inside AudioComponentInstanceNew was triggered when something like a global mutex was locked, then jumping back in a signal handler would leave the system in an unexpected state.

@t0m yep. It is a trade-off against having the app continue, or terminate and needing restarting by the user! Neither outcome is good; which one is best for the user? FWIW, I’ve seen a couple of plug-ins crash the macOS scanning in this way - Horizon, and some of the Waves ones (Horizon don’t plan to fix, and Waves are investigating).

We’re not going to deliberately introduce something into the framework that may leave the system in an undefined state. A crash at plug-in scanning time is much, much easier to diagnose than a crash somewhere else in an unrelated bit of code later on.

If people would like to choose the latter option then they can implement something along the lines of what you have proposed, which would then, hopefully, make the associated dangers more obvious.

@t0m sure, totally understood! However, it is a right pain to have an app just terminate because it hits a failing plug-in. It is a shame, but that is the way of the world when dealing with external components :slight_smile:

Pete

I know iOS is more locked down, but are you sure you can’t use a subprocess in a sandboxed macOS app?

Hi @t0m - I don’t know this categorically, and have a point to investigate at some point! :slight_smile: Once I know more, I’ll be certain to share what I find as always.

Best wishes, Pete

Hi @t0m,

I’ve started to look at what you’ve done in MainHostWindow.cpp, but I’m not finding it terribly clear at the moment. That might not be very important in the grand scheme of things for JUCE; perhaps a lot of users with plug-in hosts simply take the Audio Plugin Host and tweak it lightly to to work with their apps.

Your plug-in scanning example code - while super helpful of course - has always been quite tightly coupled-in to the UI/implementation of the Audio Plugin Host app. Last year, I found it fairly tricky to disentangle the logic of what you were doing, into my own plug-in scanning system, without dragging-in any of the code from the Audio Plugin Host. (Why did I do this? Simply, to work well with my app’s own UI flow, especially on iOS). It’d great if there were a reference plug-in scanner implementation in JUCE that were fully disentangled from the demo app :slight_smile:

Anyhow, now that you’ve modified Audio Plug-in Host to use out-of-process scanning - I can’t quite figure out how what you’ve done works. The reason I need to understand this, is that I’m going to have to apply what you’ve done to the scanning code, to what I’ve previously dis-entangled from your demo app.

Maybe my issue is simply that when poking-around with Xcode, the lldb debugger doesn’t automatically pick-up on the new process. I suspect this might be a limitation of lldb, which doesn’t seem to work terribly well with launching processes from an app. Of course, I can use Xcode to connect to the process that I see spun-up, but I don’t seem to have much success getting connected to anything happening in that process.

So, I’ll keep poking around trying to figure it out!

Pete

1 Like

@t0m I have now tried spawning-off a child process from a sandboxed (store) app for macOS, with hardened runtime, and the child process wouldn’t launch.

The reason: interprocess communication fails, when it tries to create the pipe.

    bool createFifos (bool mustNotExist)
    {
        createdFifoIn  = createFifo (pipeInName, mustNotExist);
        createdFifoOut = createFifo (pipeOutName, mustNotExist);

        return createdFifoIn && createdFifoOut;
    }

I don’t suppose you’ll ever have to time to think about doing this differently, but the following article talked about using CFMessagePort as the basis for an alternative IPC mechanism in such circumstances.

https://developer.apple.com/forums/thread/120647

Best wishes,

Pete

That article also mentions:

Another option is to create an app group that you share between your main and secondary apps. You can then create a UNIX domain socket in that app group, and communicate via that.

Did you try that with a NamedPipe?

Hi @t0m

Thanks very much for getting back to me.

I might well be missing something - nothing new there :slight_smile: - but the code in question that fails is within juce_posix_NamedPipe.cpp …

Specifically:

        return mkfifo (name.toUTF8(), 0666) == 0 || ((! mustNotExist) && errno == EEXIST);

Also the following code:

    Connection (ChildProcessCoordinator& m, const String& pipeName, int timeout)
        : InterprocessConnection (false, magicCoordWorkerConnectionHeader),
          ChildProcessPingThread (timeout),
          owner (m)
    {
        if (createPipe (pipeName, timeoutMs))
            startThread (4);
    }

… specifically creates a pipe for the IPC work - there is no obvious alternative way to have it use a socket instead.

Pete

I thought that a pipe created inside an app group might also work. If it were the sandbox that’s getting in the way then perhaps having the fifo within the app group might be allowed, just as having a file handle to a socket in the app group would be.