Nix on macOS with Cylance PROTECT

Summary

At work, I need to use the Cylance PROTECT antivirus software on my Mac. It detects the code in Nix ≥2.1 that copies paths from remote Nix stores as being a vulnerability, but it’s a false positive. The path of least resistance here was to just patch Nix to avoid triggering the false positive. I’m posting my patches here in the hopes that it will show up on a web search if somebody else hits the same problem and is also looking for a quick fix.

Details

The Cylance PROTECT antivirus software for macOS seems to detect any program using coroutines as having a “stack pivot” vulnerability. These programs are using stack pivots, but that’s just how coroutines work. I’ve looked at the offending code pretty extensively at this point, and I’m pretty sure that there’s no vulnerability here. (Security team – this means you can ignore the email I’ve sent you about this previously, in which I gave a heads up just in case there was a legit vulnerability here. I’ve investigated and there isn’t.)

Cylance started triggering after 48662d151bdf4a38670897beacea9d1bd750376a, and the fix I came up with was to just partially revert that. That change reduced memory usage used when copying from a remote store, but I’m happy to eat the extra memory use when the alternative is having my Nix be clobbered by the antivirus every time I try to use it.

Patches

Following are patches for both stable and unstable Nix that fix this problem.

Stable Nix (2.1.3)

From d0bb58ee4301f3b6abeb8ece3dbd25d93d2cdec6 Mon Sep 17 00:00:00 2001
From: Alyssa Ross <hi@alyssa.is>
Date: Wed, 14 Nov 2018 11:33:11 +0000
Subject: [PATCH] Work around Cylance's broken stack pivot check

The Cylance PROTECT antivirus software for macOS seems to detect any
program using coroutines as having a "stack pivot" vulnerability. These
programs are using stack pivots, but that's just how coroutines work.
There's no vulnerability here.

But, sometimes the path of least resistance is to just keep Cylance
happy, so this patch works around Cylance's broken behaviour by
partially reverting 48662d151bdf4a38670897beacea9d1bd750376a.
---
 src/libstore/store-api.cc | 33 +++++++++++++++++++++------------
 1 file changed, 21 insertions(+), 12 deletions(-)

diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc
index 1f42097fccf..d5a29753f0f 100644
--- a/src/libstore/store-api.cc
+++ b/src/libstore/store-api.cc
@@ -586,6 +586,26 @@ void copyStorePath(ref<Store> srcStore, ref<Store> dstStore,
 
     uint64_t total = 0;
 
+    auto progress = [&](size_t len) {
+        total += len;
+        act.progress(total, info->narSize);
+    };
+
+    struct MyStringSink : StringSink
+    {
+        typedef std::function<void(size_t)> Callback;
+        Callback callback;
+        MyStringSink(Callback callback) : callback(callback) { }
+        void operator () (const unsigned char * data, size_t len) override
+        {
+            StringSink::operator ()(data, len);
+            callback(len);
+        };
+    };
+
+    MyStringSink sink(progress);
+    srcStore->narFromPath({storePath}, sink);
+
     // FIXME
 #if 0
     if (!info->narHash) {
@@ -602,18 +622,7 @@ void copyStorePath(ref<Store> srcStore, ref<Store> dstStore,
         info = info2;
     }
 
-    auto source = sinkToSource([&](Sink & sink) {
-        LambdaSink wrapperSink([&](const unsigned char * data, size_t len) {
-            sink(data, len);
-            total += len;
-            act.progress(total, info->narSize);
-        });
-        srcStore->narFromPath({storePath}, wrapperSink);
-    }, [&]() {
-	throw EndOfFile("NAR for '%s' fetched from '%s' is incomplete", storePath, srcStore->getUri());
-    });
-
-    dstStore->addToStore(*info, *source, repair, checkSigs);
+    dstStore->addToStore(*info, sink.s, repair, checkSigs);
 }
 
 
-- 
2.18.0

Unstable Nix (9dc9b64aadadd9f3b8277fb942b00dad1a753fd3)

From af4302bf599b6d217526182d65ec6adb73e29326 Mon Sep 17 00:00:00 2001
From: Alyssa Ross <hi@alyssa.is>
Date: Wed, 14 Nov 2018 11:33:11 +0000
Subject: [PATCH] Work around Cylance's broken stack pivot check

The Cylance PROTECT antivirus software for macOS seems to detect any
program using coroutines as having a "stack pivot" vulnerability. These
programs are using stack pivots, but that's just how coroutines work.
There's no vulnerability here.

But, sometimes the path of least resistance is to just keep Cylance
happy, so this patch works around Cylance's broken behaviour by
partially reverting 48662d151bdf4a38670897beacea9d1bd750376a.
---
 src/libstore/store-api.cc | 33 +++++++++++++++++++++------------
 1 file changed, 21 insertions(+), 12 deletions(-)

diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc
index dc54c735fdb..aaa16d5a999 100644
--- a/src/libstore/store-api.cc
+++ b/src/libstore/store-api.cc
@@ -588,6 +588,26 @@ void copyStorePath(ref<Store> srcStore, ref<Store> dstStore,
 
     uint64_t total = 0;
 
+    auto progress = [&](size_t len) {
+        total += len;
+        act.progress(total, info->narSize);
+    };
+
+    struct MyStringSink : StringSink
+    {
+        typedef std::function<void(size_t)> Callback;
+        Callback callback;
+        MyStringSink(Callback callback) : callback(callback) { }
+        void operator () (const unsigned char * data, size_t len) override
+        {
+            StringSink::operator ()(data, len);
+            callback(len);
+        };
+    };
+
+    MyStringSink sink(progress);
+    srcStore->narFromPath({storePath}, sink);
+
     if (!info->narHash) {
         StringSink sink;
         srcStore->narFromPath({storePath}, sink);
@@ -608,18 +628,7 @@ void copyStorePath(ref<Store> srcStore, ref<Store> dstStore,
         info = info2;
     }
 
-    auto source = sinkToSource([&](Sink & sink) {
-        LambdaSink wrapperSink([&](const unsigned char * data, size_t len) {
-            sink(data, len);
-            total += len;
-            act.progress(total, info->narSize);
-        });
-        srcStore->narFromPath({storePath}, wrapperSink);
-    }, [&]() {
-        throw EndOfFile("NAR for '%s' fetched from '%s' is incomplete", storePath, srcStore->getUri());
-    });
-
-    dstStore->addToStore(*info, *source, repair, checkSigs);
+    dstStore->addToStore(*info, sink.s, repair, checkSigs);
 }
 
 
-- 
2.18.0
4 Likes

If your company has bought support from this company, you could maybe at least complain about this snakeoil software.

1 Like

I wonder if this issue would go away if we signed our executables? It’s actually interesting that this doesn’t break more programs. They probably maintain a huge whitelist!

2 Likes

This is immensely useful, thank you! Is there a repo/branch to build the patched version from?

No, I am deliberately not providing that, because I would then have to keep it up to date with Nix, and that would be a responsibility with no clear gain.

Just get the Nix source code from GitHub and apply the patch. Once you have a Nix built with the patch, you can use an overlay to make sure it’s applied to any updates you install with Nix:

self: super:
{
  nix = super.nix.overrideAttrs (oldAttrs: {
    patches = (oldAttrs.patches or []) ++ [
      ./nix/stable/cylance.patch
    ];
  });
  nixStable = nix;

  nixUnstable = super.nixUnstable.overrideAttrs (oldAttrs: {
    patches = (oldAttrs.patches or []) ++ [
      ./nix/unstable/cylance.patch
    ];
  });
}
1 Like

Cylance has recently started killing nix again due to the “stack pivot” violation. Is this the same issue? It has been quiet for a few months…

Cylance is awful. We had a big problem with it at work where it started randomly quarantining part of our in-development app (without any explanation) on every build, and had to get an IT policy exemption to let us uninstall it because there was no alternative.

I have a chicken-and-egg problem now: in order to build the patched nix I need functioning nix which gets killed by Cylance. Any suggestions or workarounds?

Build it on a different machine with the same OS/arch and copy it over?

If that doesn’t work, then you could always try temporarily uninstalling Cylance. At work, before getting the policy exemption, we discovered we could just uninstall Cylance ourselves (since the uninstaller was sitting on our machine next to the app); it was technically a violation of IT policy to do this, and at some point over the next 24 hours the IT management system would automatically reinstall it for us, but it was a workable short-term hack while we waited for approval. I don’t know if your system is configured to let you do this though, or if it would get you in trouble with IT.

1 Like

I don’t have either option. I though maybe there is a way to leverage TravisCI available in gitthub. There is .travis.yml in nix repository but it doesn’t have any dependencies and TravisCI seems to prefer homebrew for macOS builds.

You could construct a derivation that applies this patch to the Nix version of your choice, post it here, and ask if anyone else on macOS can build it and package up the closure to give to you.

That said, I don’t know offhand how to produce an archive of a closure (nix-copy-closure seems specific to copying to/from a remote machine), and if you don’t have a functioning Nix then I’m not sure how to even apply the closure to your machine. Unless someone knows of a way to produce a tarball that contains the full nix store paths for the whole closure so you could then just extract it at /.

1 Like

UPDATE: I managed to leverage travis-CI to build the patched version of nix and then used the overlay suggested by @qyliss to keep the future updates patched. I can share the patched binary if anybody needs it.

My solution, before I managed to get my workplace to disable stack pivot
blocking in Cylance because it started blocking Ruby, was to install Nix
2.0, which didn’t trip Cylance, and use that to build the later patched
version.