Homebrew 7.0.0; brew-vulns; Know Where They Live
This was supposed to be a Bonus Drop but getting the flu, covid, and rsv shots really made this weekend a bit of a drag.
Today, we cover the new Homebrew release (which is for both macOS and Linux!) and I blather a bit more on shell-y & forensic-y things, as I am still in mid-homelab migration.
BEFORE YOU MOVE ON! One of the western foundation model companies hacked Ruby Gems earlier this year. Homebrew relies on Ruby, but the entire Ruby ecosystem should be considered as compromised until some org does a full audit. I am still, reluctantly, running Homebrew until I hear more from my industry mates on whether we’re all borked or not.
Homebrew 7.0.0

Homebrew 7.0.0 dropped on Friday, and it’s a banger of a release. The headline items: faster parallel installs, an official native macOS GUI app (BrewUI), built-in vulnerability scanning via brew vulns, Linux sandboxing switched from Bubblewrap to Landlock, and the long-telegraphed demotion of Intel Macs to Tier 3.
The performance work impacts full install pipeline. brew install, reinstall, and upgrade now overlap package preparation and downloads – including across brew bundle batches – so you’re not sitting idle between packages waiting for the next fetch to start. brew fetch reads URLs and checksums directly from API metadata rather than loading the entire formula definition first, and brew config gathers system info concurrently instead of waiting on each subprocess in turn. On a machine with a ton of installed formulae (so, def my boxes), the difference should be noticeable (it was!).
There are eight CVEs addressed across the 6.x and 7.0.0 cycle, ranging from a high-severity issue (GHSA-rg9r-ppxp-87hm) where unsigned cask-removal metadata could execute commands with sudo – fixed in 6.0.12 by deleting the vulnerable recovery code entirely – down to a handful of low-severity redirect and path-escape issues. The 7.0.0-specific fix (GHSA-5263-whxq-77hp, Moderate) closes a sandbox escape where a malicious cask could reach outside the macOS install sandbox through LaunchServices; Homebrew now restricts application launching, Mach services, and Unix socket connections from within the sandbox. The “Installation and tap protection” section of the release notes is worth reading straight through if you run any third-party taps (and, who doesn’t?).
brew vulns and Homebrew’s new advisory database deserve more space than a roundup blurb. We’ll take a proper look at the full brew vulns surface area – the OSV.dev integration, the CC0-licensed advisory index, the --fix-available/--fix-type/--list-skipped flags and what they’re actually telling you – in an upcoming mid-section deep dive.
BrewUI ships as an official, fully released GUI (brew install homebrew-app) for macOS Tahoe 26 and later. I have no intention of using it, so do not have much blather, but I am glad they did this since I know the terminal and Homebrew itself can be daunting for folks new to the commandline.
On Linux, Bubblewrap sandboxing is gone, replaced by Landlock (kernel 5.13+). Landlock needs no extra dependencies and doesn’t require elevated Docker permissions, which Bubblewrap’s setup caused headaches with in CI environments. If your kernel predates Landlock support, you’ll get an advisory from brew doctor and fall back to the pre-6.0.0 unsandboxed configuration. The ghcr.io/homebrew/ubuntu22.04 image is also gone – migrate CI to ghcr.io/homebrew/brew.
The Intel Mac situation is what it is. Homebrew is following Apple and GitHub, both of whom are ending Intel runner support in 2027, and it’s hard to argue with the logic: if two of the largest tech companies can’t sustain it, a volunteer-run project certainly can’t fill that gap. Tier 3 means existing bottles stay available but you may end up building from source as formulae update. Full removal lands September 2027. MacPorts is the explicit recommended migration path for anyone staying on Intel hardware.
A few other items worth flagging: brew services now reads persistent environment overrides from $HOMEBREW_USER_CONFIG_HOME/services/<formula>.env, so local service config survives package upgrades. Formula post_install and cask *flight blocks are deprecated in favor of declared *_steps, with brew style --fix handling common conversions automatically; third-party taps have until December 2027 before it becomes an error. And brew install --dry-run now previews formulae and casks together in one pass, which is a small but genuinely useful quality-of-life improvement.
brew update, if you have auto-update enabled, handles the upgrade automatically.
brew-sploits

brew vulns (as noted, above) is built into 7.0.0 proper – no extra tap or gem required. It queries OSV.dev against Homebrew’s own advisory database, which records vulnerabilities at the formula version and revision level (backported fixes included), and reports anything installed that has a known outstanding issue. Flags to care about include: --severity=high, which narrows output to what actually needs attention (sort of, hence the thing I made which we cover in a bit); --fix-available and --no-fix-available, which split the list into actionable versus waiting-on-upstream; --fix-type, which distinguishes an upstream release from a backported patch; and --list-skipped which shows you the formulae from untrusted taps that the scan skipped entirely; (--deps and --brewfile round out the targeting options for more focused audits).
The advisory database itself is a separate deliverable. Homebrew publishes OSV-format records under CC0, so security teams can pull the index directly without going through Homebrew tooling – useful if you’re feeding findings into something like Grype or your own internal scanner.
For those unaware, OSV is “An open, precise, and distributed approach to producing and consuming vulnerability information for open source”. The FAQ does a fine job explaining what it is for, but — as we will see below — it’s not all guns and roses.
Homebrew also now adds upstream package identifiers to software bills of materials, meaning a formula’s SBOM entry can reference the PyPI package, npm module, or Cargo crate it wraps rather than just the Homebrew formula name.
So, brew vulns is froody and handy, but IMO it is missing a few items of import.
First, knowing abt the CVEs is great and all, but just because something is vulnerable (even with a HIGH rating) does not mean is is actively being targeted by attackers or is really even truly exploitable in common deployments. Sure, there may be cabinets full of exploits that public trackers do not have access to, but if a library or app was part of an incident, public proof-of-concept (PoC) and “was exploited” trackers will generally have that info.
Hence the creation of brew-sploits. It’s a small golang wrapper around brew vulns that parses the JSON output from it and checks a little service I have for public PoCs or evidence of exploitation. There’s a caveat in that it only checks CVEs, not OSVs, since the latter does not have a consistent/easy way to identify evidence of exploitation (ok, it sort of does, but I’m also ticked that no CNA has taken it upon themselves to get OSV-only vulns into CVE; but CVE is permabusted anyway…but, I digress…).
Running it on a freshly OS and Homebrew-updated macOS 27 system generated this output:
{ "exploited": [ "CVE-2026-37555", "CVE-2026-67215", "CVE-2026-67216", "CVE-2026-67217", "CVE-2026-88048", "CVE-2026-88049", "CVE-2026-88050", "CVE-2026-88053", "CVE-2026-88054" ], "not_exploited": [ "CVE-2025-52194", "CVE-2025-56226", "CVE-2026-16554", "CVE-2026-6192", "CVE-2026-87933", "CVE-2026-88047", "CVE-2026-88051", "CVE-2026-88052" ], "non_cve": [ "OSV-2020-2308", "OSV-2021-777", "OSV-2022-24", "OSV-2022-608", "OSV-2022-725", "OSV-2022-867", "OSV-2023-1129", "OSV-2023-1328", "OSV-2023-197", "OSV-2023-298", "OSV-2024-112", "OSV-2024-698", "OSV-2024-847", "OSV-2024-914", "OSV-2025-219", "OSV-2025-433", "OSV-2025-90", "OSV-2025-92", "OSV-2026-564", "OSV-2026-568", "OSV-2026-711", "OSV-2026-728" ], "vulnerable_packages": [ "cjson", "libsndfile", "openjpeg", "tesseract" ]}
Now I know that I should look for use of those apps/libs in new or updated bits I install on my systems before I let them run amok.
The summary has the clone URL, and feedback + patches are welcome. This post has sufficient info on how to do old-school “PRs”.
Know Where They Live

As noted, I’m still in mid-homelab migration, and still mucking with Bash scripts. I really wanted pristine scripts this time ’round, so leaned into the proper use of “where does X live?” helpers, and tossed in some notes on other “forensic”-ish tools.
Folks (including me) conflate stat, readlink, and command all the time (understandably so). These invocations get mentioned in the same breath whenever someone’s trying to answer “where does this binary actually live?” – but they do three completely different jobs, and confusing them produces scripts that fail in systems-dependent odd ways.
The confusion usually happens because the real path of a program often requires chaining all of them together.
Thus, we shall devote an entire drop to a gaggle of commands, peppering in some quick reference tables and handy command chains along the way.
stat: what the filesystem knows about a file.
stat is for metadata! NOT content; NOT the name; NOT where something points – BUT what the operating system records about the file itself: size, permissions, owner/group IDs, inode number, and the three timestamps that ls hides from you (access time, modification time, and change time, which is different from modification time in ways that absolutely matter for both forensics and caching).
stat /bin/ls
Go ahead: run it! That’ll dump file size, block count, device, inode, link count, access rights, and all three timestamps. Useful when ls -l isn’t giving you enough. Also useful for confirming whether something is a regular file or a symlink, since stat distinguishes them in its output.
You’ll want to do a man stat to see all the format sequence options (think “printf for stat) to make the best use of the metadata you need in scripts.
readlink: following the chain of pointers.
readlink does one thing – reads where a symlink points. By default it only reads one level, so if /usr/bin/python points to /usr/bin/python3, which points to /usr/bin/python3.11, you’d need to call it twice unless you add in the -f. readlink -f follows every link in the chain until it hits an actual file:
readlink /bin/sh # one level – might still be a symlinkreadlink -f /bin/sh # follows everything to the real binary
command: what the shell would actually run.
We’ve covered this in previous Drops, but it bears repeating.
command is a shell builtin, not an external binary. It interacts with the shell’s internal lookup tables – $PATH, aliases, functions – rather than going straight to the filesystem.
The flag you want is -v, which prints the path of whatever the shell would execute:
command -v python
People reach for which out of habit, and which usually works, but it’s an external program (on most distros) that can miss shell aliases or functions that command sees. For scripts, command -v is the POSIX-standard portable choice. For interactive use, which is fine.
type is the diagnostic-minded cousin. It tells you how the shell sees a command – not just where it is, but whether it’s an alias, a function, a builtin, or an external binary:
type ls # might say: ls is aliased to 'ls --color=auto'type cd # cd is a shell builtintype git # git is /usr/bin/git
whereis is the odd one in the family. Where which and command -v find executables, whereis looks for the binary and its man pages and its source files:
whereis bash# bash: /usr/bin/bash /usr/share/man/man1/bash.1.gz
More useful for “give me everything the system knows about this program’s installation” than for scripting.
Command lookup: which tool when:
| If you want to know… | Use this |
|---|---|
| “How does the shell see this – alias, builtin, or file?” | type |
| “Where’s the executable?” | command -v or which |
| “Where are the binary, man pages, and source files?” | whereis |
| “What’s the actual physical path, all symlinks resolved?” | realpath or readlink -f |
| “Is this a script, a binary, or a text file?” | file |
| “When was this last modified and what are its permissions?” | stat |
file and realpath: the overlooked two.
file looks at the file’s content – specifically the magic bytes in the header – and tells you what the thing actually is, regardless of its name or extension. Running it on /bin/ls gives you something like ELF 64-bit LSB shared object, x86-64.... Running it on a script gives you the interpreter line. Useful when you have a file with no extension, or when you suspect someone renamed a PHP script to binary.exe (this happens in incident response all the time).
realpath does what readlink -f does but with a cleaner interface – it takes any path, resolves all symlinks, collapses .. and . components, and returns the absolute canonical path. If you’re starting a greenfield script, use realpath over readlink -f. If you’re maintaining existing scripts, be careful about macOS – older macOS systems don’t have realpath by default, and you’ll need brew install coreutils to get the GNU version.
The one-liner that actually answers “where does this binary physically live” is:
readlink -f $(command -v python)# or on macOSrealpath $(command -v python)
But you can go deeper. If you want to know what a binary is after resolving it:
file $(readlink -f $(command -v python))
For scripts that need to find files relative to their own location – the pattern you want when your script is in /opt/app/bin/run.sh and needs to find /opt/app/config.conf regardless of what directory the caller is in:
SCRIPT_DIR=$(dirname $(realpath "$0"))CONFIG_FILE="$SCRIPT_DIR/config.conf"
If you suspect multiple versions of something are installed and you want to see whether they’re actually different files or just different links to the same binary:
which -a python | xargs realpath
Combination recipes:
| Goal | Command chain |
|---|---|
| Inspect a command’s full identity | file $(readlink -f $(command -v <cmd>)) |
| Make a script location-aware | SCRIPT_DIR=$(dirname $(realpath "$0")) |
| Map all installed versions | which -a <cmd> \| xargs realpath |
| Audit ELF binaries in a directory | find . -type f -executable -exec file {} + \| grep ELF \| awk '{print $1}' \| xargs stat |
Once you’ve confirmed where the binary lives and what it is, the next failure mode is usually dependencies. Most Linux binaries are dynamically linked – they need shared libraries (.so files) to run, and those libraries need to be present and findable.
ldd shows you every library a binary requires, and critically, whether any of them are missing:
ldd /bin/ls
That “cannot open shared object file” error you sometimes see when running a newly compiled or copied binary? ldd on the binary before you run it tells you in advance. On macOS, ldd doesn’t exist – you want otool -L instead, which gives you the same information in a different format.
nm goes further: it lists the symbols (functions and variables) inside a compiled binary. Generally that’s developer-level territory, but nm -D on a shared library is useful if you’re trying to confirm a specific function is actually exported before you write code against it.
Now, standard Unix permissions (owner, group, others) aren’t the only mechanism that controls access to files. Two more layers exist and they don’t show up in a normal ls listing.
lsattr exposes filesystem-level attribute flags that exist below the permission layer. The one you run into most often is the immutable flag (i), which prevents modification or deletion even by root:
lsattr /etc/shadow
If you see an i in the output, chattr -i removes it (if you have the access to do so). This is also a common ransomware indicator – certain files being made immutable when they shouldn’t be.
getfacl shows Access Control Lists, which let administrators define permissions for specific individual users or groups beyond the standard owner/group/others model. If ls -l shows a + at the end of the permissions field, an ACL is set, and getfacl tells you what it actually says:
getfacl /var/log/syslog
Neither of these has a direct equivalent on macOS. The HFS+/APFS attribute system works differently – macOS uses ls -lO for extended flags and chmod +a for ACLs, which have their own syntax.
So, now if a program called my-app is failing in a way you can’t explain, here’s the sequence that actually gets you to the answer:
command -v my-app– finds the path the shell resolves toreadlink -f [that path]– confirms the actual binary (might be several symlinks deep)file [actual path]– confirms it’s really an ELF binary and not a wrapper scriptldd [actual path]– checks for missing shared libraries (linux)lsattr [actual path]/getfacl [actual path]– rules out weird permission situations (linux)
That sequence covers roughly 90% of “it’s installed but it doesn’t work” situations without guessing.
Full toolkit summary:
| Goal | Tool | Phase |
|---|---|---|
| Find the shell’s path for a command | which / command -v | Discovery |
| Resolve symlinks to actual binary | readlink -f / realpath | Resolution |
| Check file content type | file | Identification |
| Check file metadata and timestamps | stat | Inspection |
| Check for filesystem attribute flags | lsattr | Inspection |
| Check extended ACL permissions | getfacl | Inspection |
| Check shared library dependencies | ldd / otool | Dependencies |
FIN
Remember, you can follow and interact with the full text of The Daily Drop’s free posts on:
- Mastodon via
@dailydrop.hrbrmstr.dev@dailydrop.hrbrmstr.dev - Bluesky via
<https://bsky.app/profile/dailydrop.hrbrmstr.dev.web.brid.gy>
☮️
Leave a Reply