Skip to content

Tatsh/libtether

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

libtether — native hdiutil attach / detach equivalents

C GitHub tag (with filter) License GitHub commits since latest release (by SemVer including pre-releases) CodeQL Tests Coverage Status Dependabot Stargazers pre-commit CMake Prettier

@Tatsh Buy Me A Coffee Libera.Chat Mastodon Follow Patreon

A small C library (libtether) plus two CLIs — attach-dmg and detach-dmg — that mount and unmount disk images the same way hdiutil does, by calling the same underlying macOS APIs.

Named for what it does: it tethers a disk image to the running system. Attach mounts the image as a /dev/diskN device; detach unmounts and releases it. That is the whole library — a tether you fasten and cast off, with no image creation or format manipulation.

macOS only. This depends on DiskArbitration.framework, CoreFoundation, IOKit, and the private DiskImages.framework. The CMake build hard-fails on non-Apple platforms.

How it works (and why)

Attaching a .dmg is not a DiskArbitration operation. DiskArbitration only mounts/unmounts/ejects devices that already exist. Creating the /dev/diskN backing device from an image file is done by the private DiskImages.framework. So:

Operation Framework Key call
attach (create device + mount) DiskImages.framework (private) DIHLDiskImageAttach
detach (unmount + tear down) DiskArbitration.framework (public) DADiskUnmount (whole) + DADiskEject

This split is exactly how hdiutil itself is built (its detach path is literally named unmount_and_eject).

The reverse-engineered attach API

The signature was recovered from three independent sources that all agree:

  1. The hdiutil binary (Ghidra) — call site decompiles to DIHLDiskImageAttach(optionsDict, progressCb, 0, &outDict) returning int (0 = ok).

  2. An in-the-wild caller_dihlDiskImageAttach((CFDictionaryRef)dict, nil, nil, (CFDictionaryRef*)&results) (callback + context may be nil; out-param is a CFDictionaryRef).

  3. Apple's open-source Darwin libsecurity_filevault (FVDIHLInterface.h) — the authoritative typedefs:

    typedef OSStatus _dihlDiskImageAttachProc(CFDictionaryRef inOptions,
                                              void *inStatusProc,
                                              void *inContext,
                                              CFDictionaryRef *outResults);
    typedef int  _dihlDIInitializeProc();
    typedef void _dihlDIDeinitializeProc();

    Note inStatusProc is plain void * in Apple's header, DIInitialize returns int, and DIDeinitialize returns void.

We declare these ourselves in include/diskimages_private.h and resolve them at runtime via CFBundle (see below).

Options dictionary (keys verified verbatim in hdiutil's strings)

Key Type Notes
main-url CFURLRef the image to attach (required)
shadow-url CFURLRef optional shadow file (read-write overlay)
read-only CFBoolean force read-only
agent CFString set to framework (the value hdiutil uses on this path)
verbose / quiet CFBoolean diagnostics level
skip-verify, skip-verify-locked, skip-verify-remote CFBoolean skip checksum verification

Result dictionary (*outResults)

A CFDictionaryRef whose system-entities key is a CFArray of dictionaries, each describing one attached node:

Entity key Example
dev-entry /dev/disk4s1
mount-point /Volumes/Foo
content-hint Apple_HFS

Why CFBundle instead of linking -framework DiskImages

On current macOS, private frameworks live in the dyld shared cache with no on-disk .tbd stub, so link-time -framework DiskImages typically fails. We therefore resolve the needed symbols (DIInitialize, DIDeinitialize, DIHLDiskImageAttach) at runtime — using CFBundle, exactly as Apple's own libsecurity_filevault (FVDIHLInterface.cpp) does:

CFURLRef url = CFURLCreateWithFileSystemPath(NULL,
    CFSTR("/System/Library/PrivateFrameworks/DiskImages.framework"),
    kCFURLPOSIXPathStyle, false);
CFBundleRef bundle = CFBundleCreate(NULL, url);
fp = (_dihlDiskImageAttachProc *)
     CFBundleGetFunctionPointerForName(bundle, CFSTR("DIHLDiskImageAttach"));

We also try CFBundleGetBundleWithIdentifier(CFSTR("com.apple.DiskImagesFramework")) first (that bundle identifier is present in hdiutil's strings). This needs no private headers or SDK stubs. (hdiutil itself hard-links DiskImages — which is why its import table lists the _DIHL* symbols directly — but we can't hard-link without a stub.)

Supported image formats

Because attach-dmg only hands main-url to DIHLDiskImageAttach, it attaches any format DiskImages recognizes, not just .dmg: UDIF (.dmg), sparse images (.sparseimage), sparse bundles (.sparsebundle, a directory — handled), ISO/CD masters (.iso, .cdr), NDIF, and encrypted images. The framework sniffs the format itself.

Build

cd native
cmake -S . -B build
cmake --build build

By default this builds only the library, build/libtether.a. The attach-dmg and detach-dmg CLIs are optional — enable them with -DBUILD_TOOLS=ON:

cmake -S . -B build -DBUILD_TOOLS=ON
cmake --build build

which additionally produces build/attach-dmg and build/detach-dmg.

Usage

# Attach (mount). Prints one line per attached entity: dev-entry, hint, mount-point.
./attach-dmg /path/to/Image.dmg
./attach-dmg -readwrite /path/to/Image.sparseimage
./attach-dmg -noverify -quiet /path/to/Image.dmg

# Detach (unmount + eject the whole disk). Accepts a BSD name, /dev path, or mount point.
./detach-dmg disk4
./detach-dmg /dev/disk4s1
./detach-dmg /Volumes/Foo
./detach-dmg -force -timeout 60 disk4

Layout

native/
├── CMakeLists.txt              # project, options, add_subdirectory(src/tools)
├── include/
│   ├── libtether.h             # public library API
│   └── diskimages_private.h    # reverse-engineered private DiskImages decls
├── src/
│   ├── CMakeLists.txt           # builds libtether (always)
│   ├── teth_attach.c            # attach via DIHLDiskImageAttach
│   ├── teth_detach.c            # detach via DiskArbitration unmount+eject
│   └── diskimages_private.c     # CFBundle loader for DiskImages.framework
└── tools/
    ├── CMakeLists.txt           # builds attach-dmg/detach-dmg (BUILD_TOOLS=ON)
    ├── attach_dmg.c             # attach-dmg CLI
    └── detach_dmg.c             # detach-dmg CLI

Caveats / residual risk

  • Some images may require entitlements or admin rights to attach; run with appropriate privileges if attach returns a permission error.
  • The status callback (inStatusProc) is plain void * in Apple's header and we always pass NULL, which all observed callers (filevault) do. hdiutil passes a real callback; if a future OS requires a non-NULL one, its prototype would need to be recovered.
  • Not yet compiled on macOS in this environment (developed/verified for API correctness on Linux, which has no macOS SDK). Build on macOS to validate.

About

Programmatically attach a DMG without a hdiutil subprocess.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Contributors