iMacs, Target Display Mode (TDM), and Linux

Another tale of reverse engineering

In my lab at the university, we have a couple of old-ish iMacs (around 2011). Fine machines, but the hardware is getting a bit dated - except the display itself, which is 27" of awesome. Consequently, most students now just want to use them as displays for their laptops, and with Target Display Mode in MacOS X, this (mostly) works fine. You still need a DisplayPort output on your laptop, and you need to fiddle with some Cmd-Shift-Apple-Something hotkeys, but it's basically a huge secondary display for free.

However, just like the other machines in my lab, all the iMacs are running Ubuntu Linux by default, which is installed in parallel to MacOS X. At some point, Apple will probably stop supporting these old machines anyway, so I decided to have a look into how TDM is implemented and how this functionality might be supported in Linux.

I started out by posting on Stack Overflow and asking for pointers. I only got an inexplicable downvote and nothing else, so I googled around a bit more, and found that at least part of the functionality is handled by the DisplayPort Daemon (dpd).

Next, I looked at the disassembly of dpd, without much success, and at the list of strings in the binary. There were a couple of interesting bits in there; the first lead that looked promising were things related to Display Data Channel (DDC) and Monitor Control Command Set (MCCS). So I installed ddccontrol and ddcutil and tried to get any response from the display; all I got was "DDC not supported". So no dice here.

After looking at the list of strings again, I also found a couple of references to kAppleSMCService, so I started to wonder whether the mysterious SMC might somehow be involved. After finding a SMC update that claimed to fix some issues with TDM, I was pretty sure that the SMC is indeed responsible for controlling TDM.

However, there's absolutely no official documentation on the SMC, all the publicly available information is reverse-engineered and spread out over various open-source projects (such as SMCKit or the Linux applesmc driver). There's also a very entertaining presentation by a security researcher involving Ninjas and Harry Potter (seriously). The main mode of operation is to read and write keys with 4-character identifiers, and particularly write access can trigger all sorts of internal behaviour.

After a bit of further digging, I finally found an old-ish OSX tool called smc_util, which can just dump the full list of keys and values. After getting it to compile on Yosemite, I created one such list of all key-value pairs with TDM active, one without, and finally ran diff on the result. Predictably, I got lots of differences, but thankfully, the SMC keys follow a basic convention: all T... keys are for thermal sensors, F... for fans, V.../I.../P... for voltage/current/power measurements and so on. After ignoring all of these, I ended up with the following diff:

--- tdm_active.log	2017-05-02 14:40:34.000000000 +0200
+++ tdm_inactive.log	2017-05-02 14:41:55.000000000 +0200
@@ -142,33 +142,33 @@
   MSTm  [ui8 ]  0 (bytes 00)
   MSWR  [ui8 ]  0 (bytes 00)
   MVBO  [hex_]  (bytes ff ff)
-  MVDC  [bin_]  (bytes 03)
-  MVDS  [bin_]  (bytes 2a)
-  MVE1  [si8 ]  (bytes 0e)
-  MVE5  [si8 ]  (bytes 0c)
-  MVHR  [flag]  (bytes 01)
+  MVDC  [bin_]  (bytes 00)
+  MVDS  [bin_]  (bytes 0a)
+  MVE1  [si8 ]  (bytes 0d)
+  MVE5  [si8 ]  (bytes 0b)
+  MVHR  [flag]  (bytes 00)
   MVLE  [flag]  (bytes 00)
   MVMR  [ui8 ]  0 (bytes 00)
-  MVMS  [ui8 ]  15 (bytes 0f)
+  MVMS  [ui8 ]  6 (bytes 06)
   MVT0  [si16]  (bytes 01 2c)
   MVT1  [si16]  (bytes 00 32)
   MVTD  [flag]  (bytes 00)
-  MVVS  [flag]  (bytes 01)
+  MVVS  [flag]  (bytes 00)
   NATJ  [ui8 ]  2 (bytes 02)
   NOPB  [ui8 ]  0 (bytes 00)
   NTOK  [ui8 ]  0 (bytes 00)
   ONMI  [ui8 ]  0 (bytes 00)

Interesting, but not conclusive, and I was a bit reluctant to just write any random values to these keys. In particular, MVHR [flag] 01 -> 00 stands out, but I needed a bit more context. I decided to have a look at what dpd does during runtime, so I fiddled around with two common OSX debugging tools, dtruss and dapptrace. Unfortunately, both of them generated a huge amount of output (every malloc, every strcmp, every kernel syscall etc.) and since I don't have much MacOS experience, I was at a total loss regarding what to actually look for. Luckily, I found this script for dtrace* which is exactly designed to log SMC key accesses (* basically the backend for all the other debugging tools).

Somewhat to my surprise, this immediately worked - and dpd indeed does access SMC registers, namely MVHR/MVBO/MVMR. Jackpot :-) I extended the dtrace script a bit to log more details about the SMC key accesses, and the values looked pretty consistent with what I saw earlier in the register dump. The exact sequence looks approximately as follows:

Key: MVHR, io count 1, result 00, data8 06, data32 00000000, bytes[0:1] 00 00
Key: MVHR, io count 1, result 00, data8 06, data32 00000000, bytes[0:1] 01 00
Key: MVBO, io count 2, result 00, data8 06, data32 00000000, bytes[0:1] ff ff
Key: MVMR, io count 1, result 00, data8 06, data32 00000000, bytes[0:1] 02 00

Time to actually test this in Linux. I first thought that everything I need would already be available in the applesmc driver, but it only allows read access to generic SMC keys. So after some more googling, I came across SmcDumpKey, which is a userspace tool to dump SMC keys (duh). I extended this a bit to also allow write access, and after writing 1 to MVHR and 2 to MVMR, my iMac switched to Target Display Mode. Yay :-)

Please note that this is still work in progress, e.g. after disabling TDM, you need to tell the X server to re-establish the internal DisplayPort connection by poking it with xrandr (thanks to Matthew Garrett for the hint). All the modified bits and pieces I used to cobble this together can be found on this Github repo.