#! /bin/zsh # ThunderboltUtil.sh v1.7 # by joevt Dec 5, 2024 #========================================================================================= # # # Thunderbolt DROM Notes: # # # https://lore.kernel.org/patchwork/patch/714766/ # # Macs with Thunderbolt 1 do not have a unit-specific DROM: The DROM is # empty with uid 0x1000000000000. (Apple started factory-burning a unit- # specific DROM with Thunderbolt 2.) # # Instead, the NHI EFI driver supplies a DROM in a device property. Use # it if available. It's only available when booting with the efistub. # If it's not available, silently fall back to our hardcoded DROM. # # The size of the DROM is always 256 bytes. The number is hardcoded into # the NHI EFI driver. This commit can deal with an arbitrary size however, # just in case they ever change that. # # A modification is needed in the resume code where we currently read the # uid of all switches in the hierarchy to detect plug events that occurred # during sleep. On Thunderbolt 1 root switches this will now lead to a # mismatch between the uid of the empty DROM and the EFI DROM. Exempt the # root switch from this check: It's built in, so the uid should never # change. However we continue to *read* the uid of the root switch, this # seems like a good way to test its reachability after resume. # # Background information: The EFI firmware volume contains ROM files for # the NHI, GMUX and several other chips as well as key material. This # strategy allows Apple to deploy ROM or key updates by simply publishing # an EFI firmware update on their website. Drivers do not access those # files directly but rather through a file server via EFI protocol # AC5E4829-A8FD-440B-AF33-9FFE013B12D8. Files are identified by GUID, the # NHI DROM has 339370BD-CFC6-4454-8EF7-704653120818. # # The NHI EFI driver amends that file with a unit-specific uid. The uid # has 64 bit but its entropy is much lower: 24 bit represent the model, # 24 bit are taken from a serial number, 16 bit are fixed. The NHI EFI # driver obtains the serial number via the DataHub protocol, copies it # into the DROM, calculates the CRC and submits the result as a device # property. # # # https://github.com/torvalds/linux/blob/master/drivers/thunderbolt/eeprom.c # # #define TB_DROM_DATA_START 13 # struct tb_drom_header { # /* BYTE 0 */ # u8 uid_crc8; /* checksum for uid */ # /* BYTES 1-8 */ # u64 uid; # /* BYTES 9-12 */ # u32 data_crc32; /* checksum for data_len bytes starting at byte 13 */ # /* BYTE 13 */ # u8 device_rom_revision; /* should be <= 1 */ # u16 data_len:10; # u8 __unknown1:6; # /* BYTES 16-21 */ # u16 vendor_id; # u16 model_id; # u8 model_rev; # u8 eeprom_rev; # } __packed; # # enum tb_drom_entry_type { # /* force unsigned to prevent "one-bit signed bitfield" warning */ # TB_DROM_ENTRY_GENERIC = 0U, # TB_DROM_ENTRY_PORT, # }; # # struct tb_drom_entry_header { # u8 len; # u8 index:6; # bool port_disabled:1; /* only valid if type is TB_DROM_ENTRY_PORT */ # enum tb_drom_entry_type type:1; # } __packed; # # struct tb_drom_entry_generic { # struct tb_drom_entry_header header; # u8 data[]; # } __packed; # # struct tb_drom_entry_port { # /* BYTES 0-1 */ # struct tb_drom_entry_header header; # /* BYTE 2 */ # u8 dual_link_port_rid:4; // "Dual-Link Port RID" 0 for the first Thunderbolt controller, 1 for the second Thunderbolt controller, 2 for ? # u8 link_nr:1; // "Lane" 0 for Lane 1, 1 for Lane 2 # u8 unknown1:2; // 0 # bool has_dual_link_port:1; // 1 # # /* BYTE 3 */ # u8 dual_link_port_nr:6; // "Dual-Link Port" 2,1,4,3 for ports 1,2,3,4 # u8 unknown2:2; // 0 # # /* BYTES 4 - 5 TODO decode */ # u8 micro2:4; // 0 0,1,2 (0 for first controller, 1 for second controller) # u8 micro1:4; // 4 8 # u8 micro3; // "Micro Address" 26,24 00,01 # // or "HPM Address"? # # /* BYTES 6-7, TODO: verify (find hardware that has these set) */ # u8 peer_port_rid:4; // 0 # u8 unknown3:3; // 0 # bool has_peer_port:1; // 0 # u8 peer_port_nr:6; // 0 # u8 unknown4:2; // 0 # } __packed; # # # # # # Macmini8,1 # # 0x00) CRC8: 0x.. # 0x01) UID: 0x000115CE05397601 Least significant byte is Thunderbolt Bus number (0,1,...) # 0x09) CRC32c: 0x........ # 0x0d) Device ROM Revision: 1 # 0x0e) Length: 0x.... # 0x10) Vendor ID: 0x1 two bytes; # 1 = Apple # 0x12) Device ID: 0xD two bytes; # 16=MacPro7,1 # 13=iMac19,1 or Macmini8,1 # 12=iMac18,3 or iMacPro1,1 # 11=MacBookPro11,5 // Thunderbolt 2 # 0x14) Device Revision: 0x1 # 0x15) EEPROM Revision: 0 # 0x16) 1: 8 1 0 2 8 1 00 0000 Thunderbolt Port # 0x1e) 2: 9 1 0 1 8 1 00 0000 Thunderbolt Port # 0x26) 3: 8 1 0 4 8 1 01 0000 Thunderbolt Port # 0x2e) 4: 9 1 0 3 8 1 01 0000 Thunderbolt Port # 0x36) 5: 0 9 0 1 0 0 DP or HDMI Adapter (DP In Adapter) Sink 0 # 0x3b) 6: 0 9 0 1 0 0 DP or HDMI Adapter (DP In Adapter) Sink 1 # 0x40) 7: Thunderbolt NHI Adapter # 0x42) 8: 2 0 PCIe Adapter (PCI Down Adapter) @1 (3 bits PCI Device, 5 bits for ???) # 0x45) 9: 8 0 PCIe Adapter (PCI Down Adapter) @4 # 0x48) - A: # 0x4a) - B: # 0x4c) 1: "Apple Inc." # 0x59) 2: "Macintosh" # 0x65) End # # # MacBookPro11,5 # # 0x01) UID: 0x0001001701C9F100 # 0x0d) Device ROM Revision: 1 # 0x10) Vendor ID: 0x1 # 0x12) Device ID: 0xB # 0x14) Device Revision: 0x1 # 0x15) EEPROM Revision: 1 # 0x16) 1: 8 0 0 2 8 0 00 0000 Thunderbolt Port 1, Dual Link Port, Lane 0, Dual-Link Port 2, Micro Address 0 # 0x1e) 2: 9 0 0 1 8 0 00 0000 Thunderbolt Port 2, Dual Link Port, Lane 1, Dual-Link Port 1, Micro Address 0 # 0x26) 3: 8 0 0 4 8 0 01 0000 Thunderbolt Port 3, Dual Link Port, Lane 0, Dual-Link Port 4, Micro Address 1 # 0x2e) 4: 9 0 0 3 8 0 01 0000 Thunderbolt Port 4, Dual Link Port, Lane 1, Dual-Link Port 3, Micro Address 1 # 0x36) 5: 000000000000 Thunderbolt NHI Adapter # 0x3e) 6: 6 0 PCIe Adapter (PCI Down Adapter) @3 (3 bits PCI Device, 5 bits for ???) # 0x41) 7: 8 0 PCIe Adapter (PCI Down Adapter) @4 # 0x44) 8: a 0 PCIe Adapter (PCI Down Adapter) @5 # 0x47) 9: c 0 PCIe Adapter (PCI Down Adapter) @6 # 0x4a) - A: # 0x4c) B: 500082 DP or HDMI Adapter (DP In Adapter) Sink 0, Port Affinity 1,2, Preferred Null Port 2 # 0x51) C: 500084 DP or HDMI Adapter (DP In Adapter) Sink 1, Port Affinity 3,4, Preferred Null Port 4 # 0x56) 1: "Apple Inc." # 0x63) 2: "Macintosh" # 0x6f) End # #========================================================================================= # Modify DROM iasl_location=/Applications/MaciASL.app/Contents/MacOS/iasl-stable getarrstart () { # bash arrays start at 0 # zsh arrays start at 1 (applies only to [] syntax) but this can be changed with "setopt ksh_arrays" # zsh arrays start at 0 when using ${arr:x:x} syntax local arr=(1 0) arrstart=${arr[1]} } getarrstart crc () { local bits=$(($1)) local inputreflected=$(($2)) local outputreflected=$(($3)) local polynomial=$(($4)) local crc=$(($5)) local finalxor=$(($6)) for data in $(xxd -p -c 1 | { if (( inputreflected )); then rev | tr '0123456789abcdef' '084c2a6e195d3b7f' else cat fi }); do ((crc ^= (0x$data << (bits-8)))) for ((i=0;i<8;i++)); do if ((crc >> (bits-1))); then ((crc = (crc << 1) ^ polynomial)) else ((crc <<= 1)) fi ((crc &= ((1 << $bits)-1))) done done printf "%0$(((bits+3) / 4))x" $((crc ^ finalxor)) | { if (( outputreflected )); then rev | tr '0123456789abcdef' '084c2a6e195d3b7f' else cat fi } } crc8uid () { crc 8 0 0 7 0 0xdb } crc32c () { crc 32 1 1 0x1edc6f41 0xffffffff 0xffffffff } CRCTABLE=00000000F26B8303E13B70F71350F3F4C79A971F35F1141C26A1E7E8D4CA64EB8AD958CF78B2DBCC6BE228389989AB3B4D43CFD0BF284CD3AC78BF275E133C24105EC76FE235446CF165B798030E349BD7C4507025AFD37336FF2087C494A3849A879FA068EC1CA37BBCEF5789D76C545D1D08BFAF768BBCBC2678484E4DFB4B20BD8EDED2D60DDDC186FE2933ED7D2AE72719C1154C9AC2061C6936F477EA35AA64D611580F55124B5FA6E6B93425E56DFE410E9F95C20D8CC531F97EAEB2FA30E349B1C288CAB2D1D8394623B3BA45F779DEAE05125DAD1642AE59E4292D5ABA3A117E4851927D5B016189A96AE28A7DA086618FCB05629C9BF6966EF07595417B1DBCB3109EBFA0406D4B522BEE4886E18AA3748A09A067DAFA5495B17957CBA2457339C9C6702A993584D8F2B6870C38D26CFE53516FED03A29B1F6821985125DAD3A34E59D0B01EAA244275292796BF4DCC64D4CECF77843D3B85EFBE38DBFC821C2997011F3AC7F2EBC8AC71E81C661503EE0D9600FD5D65F40F36E6F761C6936293AD106180FDE39572966096A65C047D5437877E4767748AB50CF789EB1FCBAD197448AE0A24BB5AF84F38592C855CB2DEEEDFB1CDBE2C453FD5AF467198540D83F3D70E90A324FA62C8A7F9B602C312446940115739B3E5A55230E6FB410CC2092A8FC11A7A7C35E811FF363CDB9BDDCEB018DEDDE0EB2A2F8B682982F63B78709DB87B63CD4B8F91A6C88C456CAC67B7072F64A457DC90563C5F93082F63B7FA44E0B4E91413401B7F9043CFB5F4A83DDE77AB2E8E845FDCE5075C92A8FC1760C37F1473938CE081F80FE355326B08A759E80BB4091BFF466298FC1871A4D8EA1A27DBF94AD42F0B21572CDFEB33C72D80B0C43ED04330CCBBC033A24BB5A6502036A54370C551B11B465265D122B997BAA1BA84EA524E7681D14D2892ED69DAF96E6AC9A99D9E3BC21E9DEF087A761D63F9750E330A81FC588982B21572C9407EF1CA532E023EA145813D758FE5D687E466D594B4952166DF162238CC2A06CAA7A905D9F75AF12B9CD9F2FF56BD190D3D3E1A1E6DCDEEEC064EEDC38D26C431E6A5C722B65633D0DDD5300417B1DBF67C32D8E52CC12C1747422F49547E0BBB3FFD08A86F0EFC5A048DFF8ECEE9147CA56A176FF599E39D9E1AE0D3D3E1AB21B862A832E8915CC083125F144976B4E622F5B7F572064307198540590AB964AB613A67B831C9934A5A4A909E902E7B6CFBAD787FAB5E8C8DC0DD8FE330A81A115B2B19020BD8EDF0605BEE24AA3F05D6C1BC06C5914FF237FACCF169E9F0D59B8273D688D280227AB90321AE7367CA5C18E4C94F48173DBD23943EF36E6F750105EC7612551F82E03E9C8134F4F86AC69F7B69D5CF889D27A40B9E79B737BA8BDCB4B9988C474D6AE7C44EBE2DA0A54C4623A65F16D052AD7D5351 CRC32_8 () { local crc=$1 local b=$2 local ndx=$(( (crc ^ b) & 0xFF )) printf $(( (crc >> 8) ^ 0x${CRCTABLE:$ndx*8:8} )) } # same as crc32c but faster CRC32 () { local crc=$(( 0xFFFFFFFF )); for bp in $(xxd -p -c 1); do ((crc = $(CRC32_8 $crc 0x$bp) )) done printf "%08x" $((crc ^ 0xFFFFFFFF)) } # same as CRC32 but faster CRC32b () { local crc=$(( 0xFFFFFFFF )); for bp in $(xxd -p -c 1); do (( crc = (crc >> 8) ^ 0x${CRCTABLE:$(( ((crc ^ 0x$bp) & 0xFF) * 8 )):8} )) done printf "%08x" $((crc ^ 0xFFFFFFFF)) } replacebytes () { local bytepos=$(($1*2)) local thebytes=$2 local thelen=${#thebytes} [[ -n $3 ]] && thelen=$(($3*2)) thedrom=${thedrom:0:$bytepos}${thebytes}${thedrom:$bytepos+thelen} } processdrom () { (( debug )) && echo ": processdrom " "$@" 1>&2 local dosetuid=0 local dosetport=0 local dosetstring=0 local doportnumber=-1 local dostringnumber=-1 while (( $# )); do local param="$1"; shift if [[ $param != '-' ]]; then eval "local $param=1" fi case "$param" in dosetuid) local douuidnum="" douuidnum=$(perl -pe '$_="0000000000000000" . $_;s/[^A-Fa-f0-9]//g;s/.*(.{16})$/\1/' <<< "$1" | tr 'a-f' 'A-F') # make UID uppercase like Apple does shift local dotheuid="" dotheuid=$(tr 'A-F' 'a-f' <<< "${douuidnum:14:2}${douuidnum:12:2}${douuidnum:10:2}${douuidnum:8:2}${douuidnum:6:2}${douuidnum:4:2}${douuidnum:2:2}${douuidnum:0:2}") ;; dosetport) local doportnumber="$(($1))"; shift local doportcontents="$1"; shift local doportdisable="$1" [[ $doportdisable = "-" || $doportdisable = "1" ]] && doportdisable=1 || doportdisable=0 ;; dodeleteport) local doportnumber="$(($1))"; shift ;; dosetstring) local dostringnumber="$(($1))"; shift local dostringcontents="$1"; shift ;; dodeletestring) local dostringnumber="$(($1))"; shift ;; esac done thecrc8=$((0x${thedrom:0:2})) theuid=${thedrom:2:16} theuidnum=$(tr 'a-f' 'A-F' <<< "${thedrom:16:2}${thedrom:14:2}${thedrom:12:2}${thedrom:10:2}${thedrom:8:2}${thedrom:6:2}${thedrom:4:2}${thedrom:2:2}") # make UID uppercase like Apple does if [[ $dosetuid = 1 && $theuidnum != "$douuidnum" ]]; then replacebytes 1 "$dotheuid" theuid=$dotheuid fi if [[ $theuid == 0000000000000000 ]]; then isusb4=1 theexpectedcrc8=0 else isusb4=0 theexpectedcrc8=$((0x$(xxd -p -r <<< "$theuid" | crc8uid))) fi (( theexpectedcrc8 != thecrc8 )) && { if (( dorepairchecksums )); then replacebytes 0 "$(printf "%02x" $theexpectedcrc8)" (( dodump )) && printf "0x00) CRC8: 0x%02x (changed: 0x%02x)\n" $thecrc8 $theexpectedcrc8 else (( dodump )) && printf "0x00) CRC8: 0x%02x (expected: 0x%02x)\n" $thecrc8 $theexpectedcrc8 fi } (( dodump && !isusb4 )) && { printf "0x01) UID: 0x%s // Vendor ID (USB-IF):0x%s Component ID:0x%s Router ID:0x%s" "$theuidnum" "${theuidnum:0:4}" "${theuidnum:4:11}" "${theuidnum:15:1}" if [[ $dosetuid = 1 && $theuidnum != "$douuidnum" ]]; then printf " (changed: 0x%s)" "$douuidnum" fi echo } thecrc32=$((0x${thedrom:24:2}${thedrom:22:2}${thedrom:20:2}${thedrom:18:2})) thedatalen=$((0x${thedrom:30:2}${thedrom:28:2} & 0x3ff)) theversion=$((0x${thedrom:26:2})) (( dodump )) && { printf "0x0d) Version: %d" $theversion # Device ROM Revision if (( isusb4 )); then (( theversion == 3 )) && printf " // USB4" || printf " (expected 3)" else (( theversion == 1 )) && printf " // TBT3" || printf " (expected 1)" fi printf "\n" } thereserved=$(((0x${thedrom:30:2}${thedrom:28:2} & ~0x3ff) >> 10)) (( dodump && thereserved )) && printf "0x0e) Reserved: %d (expected 0)\n" $thereserved thevendorid=0 themodelid=0 themodelrev=0 theeepromrev=0 local startentryoffset=16 if (( !isusb4 )); then thevendorid=$((0x${thedrom:34:2}${thedrom:32:2})) (( dodump )) && printf "0x10) TBT3-Vendor ID: 0x%X\n" $thevendorid themodelid=$((0x${thedrom:38:2}${thedrom:36:2})) (( dodump )) && printf "0x12) TBT3-Device ID: 0x%X\n" $themodelid themodelrev=$((0x${thedrom:40:2})) (( dodump )) && printf "0x14) TBT3-Model Revision: 0x%X\n" $themodelrev theeepromrev=$((0x${thedrom:42:2})) (( dodump )) && printf "0x15) TBT3-NVM Revision: %d\n" $theeepromrev startentryoffset=22 fi while ((1)); do local entryoffset=$startentryoffset ((thedataend = 13 + thedatalen)) # Keep a list of ports and strings and generic data Ports=() while (( entryoffset <= thedataend )); do [[ -z ${thedrom:$entryoffset*2:2} ]] && break local entrylen=$((0x${thedrom:$entryoffset*2:2})) if ((entrylen < 2)); then (( dodump && (entryoffset != thedataend) )) && echo "Unexpected error: port length is < 2: $entryoffset + $entrylen <= $thedataend" break fi (( entryoffset == thedataend )) && { (( dodump )) && echo " ============== (following bytes are unexpected)" ((thedataend += entrylen)) } local theentrytype=$((0x${thedrom:$entryoffset*2+2:2} >> 7)) # 0=generic,1=Adapter Entry local theadapterdisabled=$(((0x${thedrom:$entryoffset*2+2:2} >> 6) & 1)) # 0=enabled,1=disabled local theadapternumber=$((0x${thedrom:$entryoffset*2+2:2} & 0x3f)) # 1..9,a..d local theadapterbytes=${thedrom:$entryoffset*2+4:$entrylen*2 - 4} local actualentrylen=$((${#theadapterbytes} / 2 + 2)) if (( dodump )); then printf "0x%02x) %s %X: " $entryoffset "$( ((theadapterdisabled)) && printf "-" || printf " ")" $theadapternumber if (( theentrytype || theadapternumber < 1 || theadapternumber > 2 )); then printf "%s" "$theadapterbytes" else printf "\"%s\"" "$(perl -pE "s/(00)+$//" <<< "${theadapterbytes}" | xxd -p -r)" if [[ $(perl -pE "s/.*?((00)+)$/\1/" <<< "${theadapterbytes}") != "00" ]]; then printf " (expected single terminating null character: %s)" "$theadapterbytes" fi fi if (( theentrytype )); then if (( theadapterdisabled && entrylen != 2 )); then printf " (unexpectedly disabled)" fi else if (( theadapterdisabled )); then printf " (unexpectedly disabled)" fi fi fi if ((theadapternumber <= 0)); then (( dodump )) && printf " (expected port number > 0)" elif ((theadapternumber == (theentrytype ? doportnumber : dostringnumber))); then ((dodump)) && { ((dosetport || dosetstring)) && printf " (replaced)" || printf " (removed)" } else Ports+=("$(printf "%d %02x %02x %s %s" $((1 - theentrytype)) $theadapternumber $entryoffset $theadapterdisabled "$theadapterbytes")") fi if (( entryoffset + entrylen > thedataend )); then (( dodump )) && printf " (unexpected error: bytes exceeds expected end: 0x%02x + %d = 0x%02x > 0x%02x)" $entryoffset $entrylen $((entryoffset + entrylen)) "$thedataend" fi if (( entrylen != actualentrylen )); then (( dodump )) && printf " (unexpected error: too few remaining bytes: %d > %d)" $entrylen $actualentrylen fi if (( dodump )); then case ${isusb4}_${theentrytype}_$(printf "%02X" $theadapternumber)_${theadapterbytes} in ?_1_??_??????) local thePreferredLaneAdapter=$(( 0x0${theadapterbytes:4:2} & 0x3f )) local thePreferenceValid=$(( (0x0${theadapterbytes:4:2} >> 6) & 1 )) local theReserved=$(( (0x0${theadapterbytes:4:2} >> 7) & 1 )) printf " // DP {" ((0x${theadapterbytes:0:4})) && printf " Unknown:0x%s," "${theadapterbytes:0:4}" printf " Preferred Lane Adapter:%d, Preference Valid:%d }" "$thePreferredLaneAdapter" "$thePreferenceValid" (( theReserved )) && printf ", Reserved:%d (expected 0)" $theReserved ;; 0_1_??_????????????) printf " // TBT3-Lane Adapter { Lane:%d" "$(( (0x0${theadapterbytes:0:2} >> 4) & 1 ))" printf ", Dual-Lane Link Capable:" case $(( (0x0${theadapterbytes:0:2} >> 7) & 1 )) in 0) printf "No" ;; 1) printf "Yes" ;; esac printf ", 2nd Adapter Num:%d" "$(( (0x0${theadapterbytes:2:2} >> 0) & 0x3f ))" ((0x${theadapterbytes:4:8})) && printf ", Unknown:0x%s" "${theadapterbytes:4:8}" printf " }" ;; 0_1_??_??????????????????) printf " // TBT3-PCIe Upstream Adapter { xx:%02x.%d" \ $(( (0x0${theadapterbytes:0:2} & 0x18) | (0x0${theadapterbytes:0:2} >> 5) )) \ $(( 0x0${theadapterbytes:0:2} & 7 )) ((0x${theadapterbytes:2:16})) && printf ", Unknown:0x%s" "${theadapterbytes:2:16}" printf " }" ;; 0_1_??_??) printf " // TBT3-PCIe Downstream Adapter { xx:%02x.%d }" \ $(( (0x0${theadapterbytes:0:2} & 0x18) | (0x0${theadapterbytes:0:2} >> 5) )) \ $(( 0x0${theadapterbytes:0:2} & 7 )) ;; ?_0_01_*) printf " // ASCII Vendor Name" ;; ?_0_02_*) printf " // ASCII Model Name" ;; ?_0_08_*) local theTMUMode=$(( 0x0${theadapterbytes:0:2} & 3 )) local theTMURefresh=$(( (0x0${theadapterbytes:0:2} >> 2) & 3 )) local theReserved=$(( (0x0${theadapterbytes:0:2} >> 4) & 15 )) printf " // TMU Minimum Requested Mode { TMU Mode:" case $theTMUMode in 0) printf "Off" ;; 1) printf "Unidirectional" ;; 2) printf "Bidirectional" ;; *) printf "Reserved" ;; esac printf ", TMU Refresh Rate:" case $theTMURefresh in 1) printf "HiFi" ;; 2) printf "LowRes" ;; *) printf "Reserved" ;; esac (( theReserved )) && printf ", Reserved:%d (expected 0)" $theReserved printf " }" ;; ?_0_09_*) local theBcdUSBSpec="$(( 0x0${theadapterbytes:2:1} * 10 + 0x0${theadapterbytes:3:1} )).${theadapterbytes:0:1}.${theadapterbytes:1:1}" local theIdVendor=$((0x${theadapterbytes:6:2}${theadapterbytes:4:2})) local theIdProduct=$((0x${theadapterbytes:10:2}${theadapterbytes:8:2})) local theBcdProductFWRevision="$(( 0x0${theadapterbytes:14:1} * 10 + 0x0${theadapterbytes:15:1} )).${theadapterbytes:12:1}.${theadapterbytes:13:1}" local theTID=$((0x${theadapterbytes:22:2}${theadapterbytes:20:2}${theadapterbytes:18:2}${theadapterbytes:16:2})) local theProductHWRevision=$((0x${theadapterbytes:24:2})) printf " // Product Descriptor { USB Spec:%s, Vendor ID:0x%04x, Product ID:0x%04x, Product FW Revision:%s, TID:0x%08x, Product HW Revision:%d }" \ "$theBcdUSBSpec" "$theIdVendor" "$theIdProduct" "$theBcdProductFWRevision" "$theTID" "$theProductHWRevision" ;; ?_0_0[ACD]_*) local theLANGID=$((0x${theadapterbytes:2:2}${theadapterbytes:0:2})) # https://docs.microsoft.com/en-us/windows/win32/intl/language-identifier-constants-and-strings local theUTF16="$( xxd -p -r <<< ${theadapterbytes:4} | iconv -f UTF-16LE )" printf " // " case $(printf "0x%X" $theadapternumber) in 0xA) printf "Serial Number" ;; 0xC) printf "UTF16 Vendor Name" ;; 0xD) printf "UTF16 Model Name" ;; esac printf " { LANGID:0x%04X, _:\"%s\" }" "$theLANGID" "$theUTF16" ;; ?_0_0B_*) printf " // USB Port Mapping {" for (( usb=0; usb < ${#theadapterbytes} / 6; usb++ )); do local theUSB=${theadapterbytes:$((usb*6)):6} (( usb )) && printf "," printf " { USB3 Port Number:%d, PD Port Number:%d, xHCI Index:%d, USB Type-C:%d, USB3 Adapter Number:%d, Tunnelling Support:%d }" \ $(( (0x${theUSB:0:2} >> 0) & 0x0f )) \ $(( (0x${theUSB:2:2} >> 0) & 0x1f )) \ $(( (0x${theUSB:2:2} >> 5) & 3 )) \ $(( (0x${theUSB:2:2} >> 7) & 1 )) \ $(( (0x${theUSB:4:2} >> 0) & 0x3f )) \ $(( (0x${theUSB:4:2} >> 7) & 1 )) done printf " }" ;; ?_0_3F_*) printf " // Reserved" ;; ?_0_3*) printf " // Vendor Specific Type" ;; ?_0_*) printf " // Reserved" ;; esac echo fi ((entryoffset += actualentrylen)) done if (( doportnumber >= 0 || dostringnumber >= 0 )); then (( dosetport )) && Ports+=( "$( printf "%d %02x %02x %s %s" 0 $doportnumber 0 "$doportdisable" "$doportcontents" )" ) (( dosetstring )) && Ports+=( "$( printf "%d %02x %02x %s %s" 1 $dostringnumber 0 "0" "$(printf "%s\0" "$dostringcontents" | xxd -p -c 9999)" )" ) local allportbytes="" allportbytes=$( IFS=$'\n' for theentrystring in $(sort <<< "${Ports[*]}"); do printf "%02x%02x%s" $(( (${#theentrystring} - 10) / 2 + 2 )) $(( ((1 - ${theentrystring:0:1}) << 7) | (${theentrystring:8:1} << 6) | (0x${theentrystring:2:2} & 0x3f) )) "${theentrystring:10}" done ) replacebytes $startentryoffset "$allportbytes" ${#thedrom} ((thedatalen = ${#allportbytes}/2 + $startentryoffset - 13 )) replacebytes 14 "$(printf "%04x" "$thedatalen" | sed -E 's/(..)(..)/\2\1/')" doportnumber=-1 dostringnumber=-1 (( dodump )) && echo " after changes:" else break fi done theexpectedcrc32=$((0x$(xxd -p -r <<< "${thedrom:26:$thedatalen*2}" | CRC32b))) (( theexpectedcrc32 != thecrc32 )) && { if (( dorepairchecksums )); then replacebytes 9 "$(printf "%08x" $theexpectedcrc32 | sed -E 's/(..)(..)(..)(..)/\4\3\2\1/')" (( dodump )) && printf "0x09) CRC32: 0x%08x (changed: 0x%08x)\n" $thecrc32 $theexpectedcrc32 else (( dodump )) && printf "0x09) CRC32: 0x%08x (expected: 0x%08x)\n" $thecrc32 $theexpectedcrc32 fi } (( dodump )) && { printf "0x%02x) End" "$entryoffset" if [[ -n $(tr -d '0' <<< "${thedrom:$entryoffset*2}") ]]; then printf " (unexpected bytes: %s)" "${thedrom:$entryoffset*2}" fi echo } if (( domake )); then IFS=$'\n' entryoffset=$startentryoffset for theentrystring in $(sort <<< "${Ports[*]}"); do entrylen=$(( (${#theentrystring} - 10) / 2 + 2 )) ((entryoffset += entrylen)) done printf ' "ThunderboltDROM", Buffer (0x%X) { /* 0x00 */ 0x%s, // CRC8 checksum: 0x%02X /* 0x01 */ 0x%s, 0x%s, 0x%s, 0x%s, 0x%s, 0x%s, 0x%s, 0x%s, // Thunderbolt Bus %d, UID: 0x%s /* 0x09 */ 0x%s, 0x%s, 0x%s, 0x%s, // CRC32c checksum: 0x%08X /* 0x0D */ 0x%s, // Device ROM Revision: %d /* 0x0E */ 0x%s, 0x%s, // Length: %d (starting from previous byte) /* 0x10 */ 0x%s, 0x%s, // Vendor ID: 0x%X /* 0x12 */ 0x%s, 0x%s, // Device ID: 0x%X /* 0x14 */ 0x%s, // Device Revision: 0x%X /* 0x15 */ 0x%s, // EEPROM Revision: %d ' \ $entryoffset \ "${thedrom:0:2}" $thecrc8 \ "${thedrom:2:2}" "${thedrom:4:2}" "${thedrom:6:2}" "${thedrom:8:2}" "${thedrom:10:2}" "${thedrom:12:2}" "${thedrom:14:2}" "${thedrom:16:2}" $((0x${thedrom:2:2})) "$theuidnum" \ "${thedrom:18:2}" "${thedrom:20:2}" "${thedrom:22:2}" "${thedrom:24:2}" $thecrc32 \ "${thedrom:26:2}" $theversion \ "${thedrom:28:2}" "${thedrom:30:2}" "$thedatalen" \ "${thedrom:32:2}" "${thedrom:34:2}" $thevendorid \ "${thedrom:36:2}" "${thedrom:38:2}" $themodelid \ "${thedrom:40:2}" $themodelrev \ "${thedrom:42:2}" $theeepromrev IFS=$'\n' entryoffset=$startentryoffset for theentrystring in $(sort <<< "${Ports[*]}"); do entrylen=$(( (${#theentrystring} - 10) / 2 + 2 )) theadapternumber=$((0x${theentrystring:2:2} & 0x3f)) theadapterdisabled=${theentrystring:8:1} theentrytype=$((1 - ${theentrystring:0:1})) printf " /* 0x%02X %s %X */ 0x%02x, 0x%02x, %s" $entryoffset "$( ((theadapterdisabled)) && printf "-" || printf " " )" $theadapternumber $entrylen \ $(( (theentrytype << 7) | (theadapterdisabled << 6) | theadapternumber )) \ "$( perl -pE 's/(..)/0x\1, /g' <<< "${theentrystring:10}" )" if (( theentrytype == 0 )); then local thestring="" thestring="$(perl -pE "s/(00)+$//" <<< "${theentrystring:10}" | xxd -p -r)" if (( theadapternumber == 1 )); then printf '// Vendor Name: "%s"' "$thestring" elif (( theadapternumber == 2 )); then printf '// Device Name: "%s"' "$thestring" else printf '// "%s"' "${theentrystring:10}" fi elif (( ${#theentrystring} == 12 )); then printf '// PCIe xx:%02x.%x' $((0x${theentrystring:10} >> 5)) $((0x${theentrystring:10} & 0x1f)) #### fix this - function should only be 3 bits - therefore there are 2 unknown bits? fi printf "\n" ((entryoffset += entrylen)) done printf " },\n" fi } repairchecksums () { processdrom dorepairchecksums } setuid () { processdrom dorepairchecksums dosetuid "$1" } setport () { processdrom dorepairchecksums dosetport "$@" } deleteport () { processdrom dorepairchecksums dodeleteport "$1" } setstring () { processdrom dorepairchecksums dosetstring "$@" } deletestring () { processdrom dorepairchecksums dodeletestring "$1" } #========================================================================================= # Files from DROM dumpdrom () { processdrom dodump } makedromdsl () { processdrom domake } makedromdslall () { local savedrom="$thedrom" local thefolderpath="$1" local i="" for ((i = 1 ; i <= ${#droms[@]} ; i++)); do usedromnum "$i" makedromdsl > "${thefolderpath:=.}/${thefilenamebase}_makedromdsl.txt" done usedromstring "$savedrom" } dumpdromall () { local i="" for ((i = 1 ; i <= ${#droms[@]} ; i++)); do echo "=======================================" echo "$i)" usedromnum "$i" dumpdrom done } dumpdromalltofiles () { local savedrom="$thedrom" local thefolderpath="$1" local i="" for ((i = 1 ; i <= ${#droms[@]} ; i++)); do usedromnum "$i" dumpdrom > "${thefolderpath:=.}/${thefilenamebase}_dumpdrom.txt" done usedromstring "$savedrom" } #========================================================================================= # Use DROM cleardrominfo () { # clear anything here that is not cleared by processdrom : } usedromstring () { cleardrominfo (( debug )) && echo ": usedromstring $1" 1>&2 thedrom="$1" processdrom } usedromnum () { cleardrominfo thedrom=${droms[arrstart - 1 + $1]} processdrom thefilenamebase="${thefilenamebase}_$i" } #========================================================================================= # DROM List cleardroms () { droms=() paths=() } cleardroms adddrom () { local savedrom="$thedrom" local thepath="$1" usedromstring "$2" (( ignoreuid )) && replacebytes 0 188877665544332211 local isnew=1 local i="" for ((i = 0 ; i < ${#droms[@]} ; i++)); do if [[ $thedrom = "${droms[i+arrstart]}" ]]; then if [[ ! "$(printf "_\n%s\n_" "${paths[i+arrstart]}")" =~ $(printf ".*\n%s\n.*" "${thepath}") ]]; then paths[i+arrstart]="$(printf "%s\n%s" "${paths[i+arrstart]}" "$thepath")" fi isnew=0 break fi done if (( isnew )); then (( debug )) && echo ": isnew" 1>&2 droms+=("$thedrom") paths+=("$thepath") fi usedromstring "$savedrom" } numstrings=0 loadstring () { local sourcename="$2" if [[ -z $sourcename ]]; then ((numstrings++)) sourcename="string:$numstrings" fi adddrom "$sourcename" $(xxd -p -r <<< "$1" | xxd -p -c 99999) } loadhexfile () { adddrom "$1" "$(xxd -r "$1" | xxd -p -c 99999)" } loadonedromitem () { local dromitem="$1" local thepath="${dromitem% = <*}" local thedrom="${dromitem##* = <}" local thedrom="${thedrom%>}" (( debug )) && echo ":" adddrom "${thepath}" "${thedrom}" 1>&2 adddrom "${thepath}" "${thedrom}" } loadfwfile () { while (( $# )); do local thefilename="$1" shift IFS=$'\n' for dromitem in $( xxd -p -c 9999999999 "$thefilename" | perl -e ' while (<>) { s/(..)/\1 /g; $start = 0; foreach my $offset (0, 4096 * 3) { $start = hex(substr($_, $offset, 11) =~ s/(..) (..) (..) (..)/$4$3$2$1/r); last if ($start != hex("ffffffff")); } # look for DROM while ( /(?{$X=pos()})44 52 4f 4d 20 20 20 20 ff ff ff ff ff ff ff ff ((.. ){1008})(?{$Y=pos()})/g ) { $X /= 3; $thedrom=$1; $thedrom =~ s/(ff )*$//g; $thedrom =~ s/ //g; $version = "vers_unknown"; if ($X == hex(200)) { $version = "v" . substr($_, (10) * 3, 2); $partition = "linux"; } elsif ($X < hex(4000)) { $partition = "missing header"; } elsif ($X < hex(82000)) { $version = "v" . substr($_, (hex(4000) + 10) * 3, 2); if ($start == hex(4000)) { $partition = "active"; } elsif ($start == hex(82000)) { $partition = "inactive"; } else { $partition = "unknown start"; } } elsif ($X < hex(100000)) { $version = "v" . substr($_, (hex(82000) + 10) * 3, 2); if ($start == hex(4000)) { $partition = "inactive"; } elsif ($start == hex(82000)) { $partition = "active"; } else { $partition = "unknown start"; } } else { $partition = "rom too large"; } pos() = $Y; $nvmversion = "nvm_unknown"; # get nvm version from after EE_PCIE if ( /(?{$Z=pos()})45 45 5f 50 43 49 45 20 (?:ff ){8}(?:.. )*?(..) (..) (..) (..) (?:.. ){4}(?:ff ){8}(?{$W=pos()})/g ) { pos() = $W; # need to find out what all the digits are for - I am just guessing here: $nvmversion="nvm_v" . (( $2 . $1 . "." . $3 . $4 ) =~ s/0*([^.]*.)\.0*(.+)/\1.\2/r ); } else { pos() = $Y; } printf ("%s:%s:%s:%s:0x%x = <%s>\n", "'"${thefilename}"'", $partition, $version, $nvmversion, $X, $thedrom); } } ' ); do (( debug )) && echo ":" loadonedromitem "${dromitem}" 1>&2 loadonedromitem "${dromitem}" done done } loadonedslfile () { local thefilename="$1" local thesource="$2" [[ -z $thesource ]] && thesource="$thefilename" # # LeadNameChar := [A-Za-z_] # DigitChar := [0-9] # NameChar := [A-Za-z_0-9] # RootChar := \ # ParentPrefixChar := ^ # PathSeparatorChar := . # # // Names and paths # # NameSeg := [A-Za-z_][A-Za-z_0-9]{0,3} # # PrefixPath := \^* # # NamePathTail := ([.][A-Za-z_][A-Za-z_0-9]{0,3})* # # NamePath := (([A-Za-z_][A-Za-z_0-9]{0,3}([.][A-Za-z_][A-Za-z_0-9]{0,3})*)|) # # NonEmptyNamePath := [A-Za-z_][A-Za-z_0-9]{0,3}([.][A-Za-z_][A-Za-z_0-9]{0,3})* # # NameString := ([\\]|\^+|)(([A-Za-z_][A-Za-z_0-9]{0,3}([.][A-Za-z_][A-Za-z_0-9]{0,3})*)|) # | [A-Za-z_][A-Za-z_0-9]{0,3}([.][A-Za-z_][A-Za-z_0-9]{0,3})* # IFS=$'\n' for dromitem in $( perl -e ' use strict; my $line3 = ""; my $line2 = ""; my $line1 = ""; my %paths = ( "\\" => 1 ); sub addline { $line3 = $line2; $line2 = $line1; $line1 = $_[0]; } sub group { my $indent = $_[0]; my $codepath = $_[1]; my $acpipath = $_[2]; my $nextacpipath = $acpipath; my $nextcodepath = $codepath; my $dodump = 0; my $buffersize = 0; if ( $line2 =~ /"ThunderboltDROM",\s*$/) { $dodump = 1; if ( $line1 =~ /^\s*Buffer\s*\(0x([0-9A-F]+)\)/ ) { $buffersize = hex($1); } elsif ( $line1 =~ /^\s*Buffer\s*\(One\)/ ) { $buffersize = 1; } elsif ( $line1 =~ /^\s*Buffer\s*\(Zero\)/ ) { $buffersize = 0; } else { $dodump = 0; } } if ( $dodump == 1 ) { print "'"$thesource"':" . $acpipath . ":ThunderboltDROM = <"; } while (<>) { s/\s*\/\/.*//; # single line // comment s/\s*\/\*.*?\*\/\s*//g; # single line /* */ comment s/\s*(.*?)\s*$/\1/g; # remove indents and trailing spaces if (/^\/\*.*/ .. /^.*?\*\/.*/) { } # multi line comment elsif ( /^$/ ) { } # skip blank lines elsif ( /^{$/ ) { group($indent . "\t", $nextcodepath, $nextacpipath); } elsif ( /^}/ ) { if ( $dodump == 1 ) { if ($buffersize < 0) { print STDERR "Buffer size is too small\n"; } elsif ($buffersize > 0) { print "00"x$buffersize; } print ">\n"; $dodump = 0; } last; } else { addline($_); if ( /(External|Device|Field|Method|Name|Scope)\s*\(\s*([\\]|\^+|)(([A-Za-z_][A-Za-z_0-9]{0,3}([.][A-Za-z_][A-Za-z_0-9]{0,3})*)|)/ ) { my $command = $1; my $prefix = $2; # \ or ^+ or empty my $nameparts = $3; $nameparts =~ s/([A-Za-z0-9])_+/\1/g; # remove trailing spaces #print "nameparts:" . $nameparts . "\n"; if ( $prefix =~ /\\/ ) { # absolute path $nextacpipath = $prefix . $nameparts; } elsif ( $prefix =~ /\^+/ ) { # parent path - remove from the current path a number of parents equal to the number of ^ characters my $pattern = "(.*?)([\\\\.][^.]+){0," . length($prefix) . "}\$"; # empty path means root "\" $nextacpipath = ($acpipath =~ s/$pattern/\1/r =~ s/^$/\\/r) . "$nameparts"; } else { # assume path is current path appended with name $nextacpipath = (($acpipath . "." . $nameparts) =~ s/^\\\./\\/r); # "\." is "\" if ( ($command eq "Scope") && ($nameparts =~ /[^.]+/) && !($paths{$nextacpipath}) ) { # For Scope, if path does not exist... if ( ($acpipath . ".") =~ /(.*[.\\]$nameparts)[.].*/ ) { # if current path includes names then set scope to parent that ends at name $nextacpipath = (($acpipath . ".") =~ s/(.*[.\\]$nameparts)[.].*/\1/r) } else { # unknown path - assume it exists print STDERR "unknown path " . $nextacpipath . "\n"; #print STDERR "$_\n" for keys %paths; } } } # code path is just current path appended with entire prefix/nameparts (no interpretation of prefix characters or scoping) $nextcodepath = (($codepath . "." . $prefix . $nameparts) =~ s/^\\\./\\/r =~ s/\\\\/\\/r); # "\." is "\" and "\\" is "\" if ( !$paths{$nextacpipath} ) { # keep a list of all paths $paths{$nextacpipath} = 1; } } else { $nextacpipath = $acpipath; $nextcodepath = $codepath; if ( $dodump == 1 ) { my $outline = ($line1 =~ s/0x//gr =~ s/[, ]//gr); print $outline; $buffersize -= length($outline) / 2; } } } } } group("", "\\", "\\"); # print "$_\n" for keys %paths; ' < "$thefilename" ); do (( debug )) && echo ":" loadonedromitem "${dromitem}" 1>&2 loadonedromitem "${dromitem}" done } loaddslfile () { while (( $# )); do local thefilename="$1" shift loadonedslfile "$thefilename" done } loadamlfile () { [[ -f $iasl_location ]] || { echo "# Update iasl_location" 1>&2 exit 1 } thedirname=$(mktemp -d /tmp/aml.XXXXXX) || exit 1 while (( $# )); do local thefilename="$1" shift if "$iasl_location" -p "$thedirname/xxx" "$thefilename" > /dev/null 2> "$thedirname/out.txt"; then loadonedslfile "$thedirname/xxx.dsl" "$thefilename" else cat "$thedirname/out.txt" 1>&2 fi done } loadoneioregfile () { local thefilename="$1" local thesource="$2" [[ -z $thesource ]] && thesource="$thefilename" IFS=$'\n' for dromitem in $( perl -e ' $inhex=0; $thepath=""; while (<>) { if ( $inhex && /^[ |]*[0-9A-F]+:((?: [0-9A-F]{2}){1,32})/ ) { $thehex .= $1 } elsif ( $inhex ) { $inhex = 0; print "" . (lc ($thehex =~ s/ //gr)) . ">\n" } if ( /^([ |]*)\+\-o (.+) /i ) { print "'"${thesource}"'" . ":" . $thepath . "/" . $1 . " = <" . $2 . ">\n" } elsif ( /^[ |]*"(ThunderboltDROM|thunderbolt-drom|DROM)" = $/i ) { print "'"${thesource}"'" . ":" . $thepath . "/" . $1 . " = <"; $inhex=1; $thehex="" } } ' < "${thefilename}" ); do #echo "«${dromitem}»" loadonedromitem "${dromitem}" done } loadioregfile () { while (( $# )); do local thefilename="$1" shift loadoneioregfile "$thefilename" done } loadioreg () { local tmpfilename="" tmpfilename=$(mktemp /tmp/local_ioreg.XXXXXX) || exit 1 ioreg -lw0 > "$tmpfilename" loadoneioregfile "$tmpfilename" "ioreg" } listdroms () { local savedrom="$thedrom" local i="" for ((i = 1 ; i <= ${#droms[@]} ; i++)); do usedromnum "$i" echo "$i)" echo "thedrom=$thedrom" echo "sources:" echo "${paths[i+arrstart-1]}" echo done usedromstring "$savedrom" } #========================================================================================= # Misc extractfwfromexe () { while (( $# )); do local thefilename="$1" shift perl -0777 -e ' while (<>) { #printf "file:%s\n", $ARGV; @files = ( /\0([^\0]+.bin)(?=\0)/g ); $filenum = 0; while ( /(?{$x=pos()})DROM(?{$y=pos()})/g ) { $sb = substr $_, $x - 0x4200 - 4, 4; $size = (ord(substr $sb, 3, 1) << 24) | (ord(substr $sb, 2, 1) << 16) | (ord(substr $sb, 1, 1) << 8) | ord (substr $sb, 0, 1); $filename = @files[$filenum]; if ( $filename =~ /^$/ ) { $filename = "fw.bin"; } $dir = sprintf "%s/%02d", `dirname "$ARGV"` =~ s/\n$//r, $filenum; `mkdir -p "$dir"`; open(FH, ">", "$dir/$filename") or die $!; print FH substr $_, $x - 0x4200, $size; close(FH); pos() = $y; $filenum++; } } ' "$thefilename" done } #========================================================================================= # Help dromhelp () { echo $' Commands: Get DROM loadioreg loadstring hexstring [sourcename] loadfwfile filepath... loadamlfile filepath... loaddslfile filepath... loadioregfile filepath... loadhexfile filepath # for reverses xxd listdroms cleardroms Use DROM usedromnum numberfromlist usedromstring lowercasehexstring Modify DROM repairchecksums replacebytes bytepos lowercasehexstring [numbytestoreplace] setuid newuid setport 0xportnumber portcontents [-] deleteport 0xportnumber setstring 0xstringnumber stringvalue deletestring 0xstringnumber Files from DROM dumpdrom makedromdsl makedromdslall [folderpath] dumpdromall dumpdromalltofiles [folderpath] Misc extractfwfromexe exepath... # $(grep -l --include \'*.exe\' -R \'DROM\' . ) Variables dodump # Set to 1 to dump the DROM while changes are made to the DROM. debug # Set to 1 to output debugging info (uses stderr) ignoreuid # Set to 1 to replace all uids with 0x1122334455667788. # DROMs with the same contents except UID will be considered identical. Help dromhelp ' } #dromhelp #=========================================================================================