es_mute_path() vs. deprecated es_mute_path_literal() - incompatibility and wrong documentation

I recently upgraded a line of code in my Endpoint-Security client, to remove a deprecation warning:

 
for (NSString *mutePath in ignoredBinaryPaths) {
//(old)  res = es_mute_path_literal(self.esClient, [mutePath UTF8String]);
    res = es_mute_path(self.esClient, [mutePath UTF8String], ES_MUTE_PATH_TYPE_TARGET_LITERAL);
    if (res!=ES_RETURN_SUCCESS)
        os_log_error(setupLog, "Failed to white-list binary:%{public}@ error:%{errno}d", mutePath, errno);
}

However, after this change, I started receiving tons of ES event messages, for AUTH_OPEN and AUTH_CREATE and many others, from processes/executables I explicitly and successfully muted! Since ES is so performance sensitive - I got worried.

Inspecting better the new API I found incoherent documentation and even misleading and contradicting definitions.

But the ES headers say differently!!!

/**
 * @brief Suppress all events matching a path.
 *
 * @param client The es_client_t for which the path will be muted.
 * @param path The path to mute.
 * @param type Describes the type of the `path` parameter.
 *
 * @return es_return_t A value indicating whether or not the path was successfully muted.
 *
 * @note Path-based muting applies to the real and potentially firmlinked path
 *       of a file as seen by VFS, and as available from fcntl(2) F_GETPATH.
 *       No special provisions are made for files with multiple ("hard") links,
 *       or for symbolic links.
 *       In particular, when using inverted target path muting to monitor a
 *       particular path for writing, you will need to check if the file(s) of
 *       interest are also reachable via additional hard links outside of the
 *       paths you are observing.
 *
 * @see es_mute_path_events
 * @discussion When using the path types ES_MUTE_PATH_TYPE_TARGET_PREFIX and ES_MUTE_PATH_TYPE_TARGET_LITERAL Not all events are
 * supported. Furthermore the interpretation of target path is contextual. For events with more than one target path (such as
 * exchangedata) the behavior depends on the mute inversion state Under normal muting the event is suppressed only if ALL paths
 * are muted When target path muting is inverted the event is selected if ANY target path is muted For example a rename will be
 * suppressed if and only if both the source path and destination path are muted. Supported events are listed below. For each
 * event the target path is defined as:
 *
 * EXEC: The file being executed
 * OPEN: The file being opened
 * MMAP: The file being memory mapped
 * RENAME: Both the source and destination path.
 * SIGNAL: The path of the process being signalled
 * UNLINK: The file being unlinked
 * CLOSE: The file being closed
 * CREATE: The path to the file that will be created or replaced
 * GET_TASK: The path of the process for which the task port is being retrieved
 * LINK: Both the source and destination path
 * SETATTRLIST: The file for which the attributes are being set
 * SETEXTATTR: The file for which the extended attributes are being set
 * SETFLAGS: The file for which flags are being set
 * SETMODE: The file for which the mode is being set
 * SETOWNER: The file for which the owner is being set
 * WRITE: The file being written to
 * READLINK: The symbolic link being resolved
 * TRUNCATE: The file being truncated
 * CHDIR: The new working directory
 * GETATTRLIST: The file for which the attribute list is being retrieved
 * STAT: The file for which the stat is being retrieved
 * ACCESS: The file for which access is being tested
 * CHROOT: The file which will become the new root
 * UTIMES: The file for which times are being set
 * CLONE: Both the source file and target path
 * FCNTL: The file under file control
 * GETEXTATTR The file for which extended attributes are being retrieved
 * LISTEXTATTR The file for which extended attributes are being listed
 * READDIR The directory for whose contents will be read
 * DELETEEXTATTR The file for which extended attribues will be deleted
 * DUP: The file being duplicated
 * UIPC_BIND: The path to the unix socket that will be created
 * UIPC_CONNECT: The file that the unix socket being connected is bound to
 * EXCHANGEDATA: The path of both file1 and file2
 * SETACL: The file for which ACLs are being set
 * PROC_CHECK: The path of the process against which access is being checked
 * SEARCHFS: The path of the volume which will be searched
 * PROC_SUSPEND_RESUME: The path of the process being suspended or resumed
 * GET_TASK_NAME: The path of the process for which the task name port will be retrieved
 * TRACE: The path of the process that will be attached to
 * REMOTE_THREAD_CREATE: The path of the process in which the new thread is created
 * GET_TASK_READ: The path of the process for which the task read port will be retrieved
 * GET_TASK_INSPECT: The path of the process for which the task inspect port will be retrieved
 * COPYFILE: The path to the source file and the path to either the new file to be created or the existing file to be overwritten
 */

So the behavior completely changed, you can no longer specify executables (via their binary path) from which you do NOT want any events

Muting effectively became reactive, not proactive.

Why this change is not documented with the deprecation? Why no alternative is suggested? why find this only because it broke my software tool behavior and performance?

And last: For how long can I rely on the old, deprecated APIs, should I choose to revert my change instead of devising a whole new mechanism for muting un-interesting

Answered by DTS Engineer in 851376022

But when I use the newer API (that you say is identical to the old, or rather that the 'old' API translates to the newer)

This caught my eye so I went back over your code and the problem is that you're NOT making the same API call. Here is the code from your original post:

res = es_mute_path(self.esClient, [mutePath UTF8String], ES_MUTE_PATH_TYPE_TARGET_LITERAL);

You passed "ES_MUTE_PATH_TYPE_TARGET_LITERAL", which means:

ES_MUTE_PATH_TYPE_TARGET_LITERAL-> The command targets this specific <path>

You should have passed "ES_MUTE_PATH_TYPE_LITERAL", which means:

ES_MUTE_PATH_TYPE_LITERAL-> The executable path is <path>

I actually copied that mistake in my previous reply so, correcting my mistake, these calls are direct equivalents.

res = es_mute_path_literal(...);

and

res = es_mute_path(..., ES_MUTE_PATH_TYPE_LITERAL);

Finally, on this point:

I don't know how to extract a simple C project to demonstrate the issue, because my code is big and quite complicated, but I will make an attempt to provide such a sample.

The best way to do this is to start with the "Monitoring System Events with Endpoint Security" sample project and then modify that project to replicate the problem. That gives you a simple starting point to build off of and it also means that most of the code is already "ours".

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

All the mute APIs are just wrapper around es_mute_path_events(). es_mute_path() and es_mute_path_literal() are the same thing. They both just call es_mute_path_events() but _literal() passes ES_MUTE_PATH_TYPE_LITERAL for the mute type.

So the behavior completely changed, you can no longer specify executables (via their binary path) from which you do NOT want any events

This is not the case.

there are 4 types of path mutes:

  • PATH_LITERAL
  • PATH_PREFIX
  • TARGET_PATH_LITERAL
  • TARGET_PATH_PREFIX

PATH_LITERAL matches the behaviour of es_mute_path_literal() exactly. In fact it's literally the same code. The header doc you have quoted is describing the behaviour of all 4 types of path muting. Target path muting behaves very differently.

If you are receiving events you have muted please provide a minimal reproduction (ideally in C) and I'll investigate it.

However, after this change, I started receiving tons of ES event messages, for AUTH_OPEN and AUTH_CREATE and many others, from processes/executables I explicitly and successfully muted!

I cannot explain what happened, but I don't think it was the API change from es_mute_path_literal to es_mute_path. In terms of API behavior, these two calls:

1) es_mute_path_literal(esClient, path);

2) es_mute_path(esClient, path, ES_MUTE_PATH_TYPE_TARGET_LITERAL);

...have EXACTLY the same behavior. I mean that quite literally. I've looked at our code and both of these two functions call into EXACTLY the same internal function and the ONLY difference between their implementations is that "es_mute_path_literal" hard codes the value "ES_MUTE_PATH_TYPE_TARGET_LITERAL" while "es_mute_path" passes in whatever you passed into it.

Similarly:

But the ES headers say differently!!!

First, as a general comment, the EndpointSecurity headers should be considered the canonical documentation for this API. The EndpointSecurity team puts a great deal of time and effort into making sure that the headers describe as accurately as possible the full details of the API’s behavior. If you haven't looked at them before, I would strongly recommend reviewing them, as they are the best documentation currently available.

In terms of the specific section you referenced, es_mute_path was actually created as part of the broader expansion of the muting system, which introduced es_mute_path_events. As part of that expansion, the EXACT behavior of muting was documented in far more detail, which is when that header doc was written. The key point here is that the header is actually describing how the API "works", NOT documenting significant changes.

Critically, all of that happened in macOS 12 and hasn't really changed since then.

Why is this change not documented with the deprecation? Why is no alternative suggested? Why do I find this only because it broke my software tool behavior and performance?

As I described above, I don't know what's causing the issue you're seeing, but I do know that it isn't because of any difference between es_mute_path_literal and es_mute_path. I think something else is going on that you should take a closer look at.

And last: For how long can I rely on the old, deprecated APIs? Should I choose to revert my change instead of devising a whole new mechanism for muting uninteresting?

In general, we don't comment on our future plans. What I will say is:

  • The primary reason it was deprecated was that es_mute_path provided identical functionality with better API flexibility. The point of the deprecation was to call attention to the superior alternative, not because of any inherent issue or flaw.

  • The reason it hasn't been removed is that there is essentially no meaningful maintenance "cost" or downside to leaving it in place. Frankly, the requirements of binary compatibility mean that removing it would actually require more work than leaving it alone.

  • Related to the previous point, I'd only see us actually removing it as part of a major system release ("macOS 26"), not as part of a minor system update ("macOS 15.x").

In summary, I certainly can't promise it will continue to work indefinitely, but I also don't see any reason we'd actively remove it.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

For one thing, though, I need clarification.

the "path literal" that I mute.

Is it, or is it not, the path of the EXECUTABLE.

My need is simple: I do not want to receive any event for specific executables (OS processes I know to be benign and whose events I do not want to evaluate and authorize).

Isn't there an API to do that? must I receive some event from them and then dynamically mute the process?

For one thing, though, I need clarification. the "path literal" that I mute. Is it, or is it not, the path of the EXECUTABLE?

I think the confusion here is that you're focusing too much on the word "literal". Within the API, "literal" just means "exact match". The constants are actually structured as:

(1) Match with the "source" of the command/executable:

ES_MUTE_PATH_TYPE_LITERAL-> The executable path is <path>
ES_MUTE_PATH_TYPE_PREFIX-> The executable path starts with <path>

(2) Match with the "target" (meaning, the thing the command is trying to "effect") of the command:

TARGET_PATH_LITERAL-> The command targets this specific <path>
TARGET_PATH_PREFIX-> The command targets a path that starts with <path>

Note that the second case is more complicated than the first since, for example, a command like "rename" has two targets (source and destination, both of which could be "interesting"). Hence the comment:

 * @discussion When using the path types ES_MUTE_PATH_TYPE_TARGET_PREFIX and ES_MUTE_PATH_TYPE_TARGET_LITERAL Not all events are
 * supported. Furthermore the interpretation of target path is contextual. For events with more than one target path (such as
 * exchangedata) the behavior depends on the mute inversion state. Under normal muting, the event is suppressed only if ALL paths
 * are muted. When target path muting is inverted, the event is selected if ANY target path is muted. For example, a rename will be
 * suppressed if and only if both the source path and destination path are muted. Supported events are listed below. For each
 * event the target path is defined as:

My need is simple: I do not want to receive any event for specific executables (OS processes I know to be benign and whose events I do not want to evaluate and authorize). Isn't there an API to do that?

Yes. That API is:

es_mute_path(esClient, path, ES_MUTE_PATH_TYPE_TARGET_LITERAL);

Must I receive some event from them and then dynamically mute the process?

...with a few qualifiers:

  • The nature of the file system means that it's possible to create multiple paths to the same object, which could end up bypassing a mute you've previously configured.

  • For performance/memory reasons, some ES state is managed "globally" (across ES clients), so it's possible for changes on an ES client to affect another client.

  • It's entirely possible that there is some other edge/detail/change/bug* that might cause us to report an event as well.

*For example, I don't know that will happen if an executable is moved from an unmuted location to a muted location, but I don't think it would be "wrong" if some events still arrived from that process after it was moved.

In other words, my recommendation here would be that you use es_mute_path to mute whatever you can but that "backstop" that with process muting if/when you hear from a process you don't "expect" to hear from.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

The edge cases and qualifiers you provide here are very far from my needs. Most executables I want to mute are well protected by SIP, and will never move (MacOS private frameworks, daemons etc. plus few third-party that are also very well defined).

My issue is very blunt. I mute a simple and perfect executable path.

Here are some - my list is much longer, but you'll get the idea:

+(NSSet<NSString *> *) baseBinaryPathWhitelist {
    static NSSet *_baseBinaryPath = nil;    // any executable in these specific paths will be ignored
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _baseBinaryPath = [NSSet setWithArray: @[
            NSProcessInfo.processInfo.arguments[0],         // don't inspect our own ITProtector process.
            @"/sbin/launchd",
            @"/bin/launchctl",
            @"/bin/ps",
            @"/bin/sleep",

            @"/usr/bin/dscl",
            @"/usr/bin/log",
            @"/usr/bin/vmmap",
            @"/usr/sbin/syslogd",
            @"/usr/sbin/spindump",
            @"/usr/sbin/usernoted",
            @"/usr/sbin/securityd",
            @"/usr/sbin/ipconfig",
            
            @"/usr/libexec/biomesyncd",
            @"/usr/libexec/logd"];  

But when I use the newer API (that you say is identical to the old, or rather that the 'old' API translates to the newer)

Then I get lots of ES events from, for example

        @"/usr/sbin/ipconfig"

Although I know for a fact that it was muted upfront, and that calling the muting API returned successfully.

The strangest thing is, if I revert to the deprecated API - and that is ALL that I do, just change a single line of code -- then these events are NOT received anymore. Same array, same executables, no other change.

I don't know how to extract a simple C project to demonstrate the issue, because my code is big and quite complicated, but I will make an attempt to provide such sample.

Accepted Answer

But when I use the newer API (that you say is identical to the old, or rather that the 'old' API translates to the newer)

This caught my eye so I went back over your code and the problem is that you're NOT making the same API call. Here is the code from your original post:

res = es_mute_path(self.esClient, [mutePath UTF8String], ES_MUTE_PATH_TYPE_TARGET_LITERAL);

You passed "ES_MUTE_PATH_TYPE_TARGET_LITERAL", which means:

ES_MUTE_PATH_TYPE_TARGET_LITERAL-> The command targets this specific <path>

You should have passed "ES_MUTE_PATH_TYPE_LITERAL", which means:

ES_MUTE_PATH_TYPE_LITERAL-> The executable path is <path>

I actually copied that mistake in my previous reply so, correcting my mistake, these calls are direct equivalents.

res = es_mute_path_literal(...);

and

res = es_mute_path(..., ES_MUTE_PATH_TYPE_LITERAL);

Finally, on this point:

I don't know how to extract a simple C project to demonstrate the issue, because my code is big and quite complicated, but I will make an attempt to provide such a sample.

The best way to do this is to start with the "Monitoring System Events with Endpoint Security" sample project and then modify that project to replicate the problem. That gives you a simple starting point to build off of and it also means that most of the code is already "ours".

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

That should be it. Thank you. But I urge you to re-read the "canonical" documentation in the headers. The difference you point here (between PATH_TYPE_TARGET_LITERAL and PATH_TYPE_LITERAL is not written there.

The very simple distinction between "the path of the executable creating the event" and "the path of the file which is the target of the event" -- is simply not there.

I understand now that these paths can sometimes coincide, but I think the documentation lacks proper explanation for the "use case" of these 4 different path "styles".

Thank you - I will test with the newer API with the (hopefully correct) path style, and see that indeed events are muted.

es_mute_path() vs. deprecated es_mute_path_literal() - incompatibility and wrong documentation
 
 
Q