I²C Write Bug in Ocean Optics Spectrometers


Problem statement

In a previous post I mentioned that the General I²C Write (0x61) command has a firmware bug.

This post lays out the supporting evidence for that claim, with plenty of details.

Background: I²C scanning, General I²C Write command

This is a slight repeat from the previous post, feel free to skip.

In order to scan the I²C bus, you iterate over addresses between 0x08 and 0x77, and for each you attempt zero-length I²C write transaction. If you get an ACK, there’s a device. Otherwise there isn’t.

Ocean Optics firmware has a handy 0x61 function for I²C writes:

General I²C Write command description in datasheet Screenshot of General I²C Write command description from HR4000 datasheet

So one might think you can use it for this. And you could, if it weren’t for…

The bug

Unfortunately the General I²C Write (0x61) command does only seem to work well for payloads that are of length 1..61.

The upper range is documented in the description of General I²C Read (0x60) command. The minimum is not mentioned.

What happens when you issue a write with zero length is terrifying.

Let me demonstrate. The following script issues writes with the length of 1, 2, 0, 64, 128, 253:

import seabreeze.spectrometers as sb
import struct
import time

s = sb.Spectrometer.from_first_available()

def i2cwrite(target, data=b""):
    s.f.raw_usb_bus_access.raw_usb_write(
            data=struct.pack('<BBB', 0x61, target, len(data)) + data,
            endpoint='primary_out')
    return s.f.raw_usb_bus_access.raw_usb_read('primary_in', 1)[0]

target = 0x53 # secondary flash

for i in [1, 2, 0, 64, 128, 253]:
    res = i2cwrite(target, bytes(range(0,i)))
    print(f"result: {res!r}")
    time.sleep(0.1)

usbmon

To figure out whether commands are sent correctly, I dumped the data using usbmon.

If you don’t know, it’s a Linux kernel debugging facility. You set it up as follows:

# Setup
$ mount -t debugfs none_debugs /sys/kernel/debug
$ modprobe usbmon
# Get device bus + id
$ lsusb | grep HR4000
Bus 003 Device 009: ID 2457:1012 Ocean Optics Inc. HR4000 Spectrometer
# Get the traffic...
$ grep --line-buffered 3:009 /sys/kernel/debug/usb/usbmon/3u

and out streams all USB communication with the device.

In this case you get (I have grepped out the relevant bits, full usbmon.txt here):

$ grep = test-0x61-bug-usbmon.txt | sed 's/05060708 .*/[...]/'
ffff8b940f2ad840 613568586 S Bo:3:009:1 -115 4 = 61530100
ffff8b940f2ad840 613569041 C Bi:3:009:1 0 1 = 08
ffff8b940f2ad840 613669521 S Bo:3:009:1 -115 5 = 61530200 01
ffff8b940f2ad840 613669974 C Bi:3:009:1 0 1 = 08
ffff8b940f2ad840 613770453 S Bo:3:009:1 -115 3 = 615300
ffff8b940f2ad840 613782616 C Bi:3:009:1 0 1 = 08
ffff8b940f2ad840 613883102 S Bo:3:009:1 -115 67 = 61534000 01020304 [...]
ffff8b940f2ad840 613886338 C Bi:3:009:1 0 1 = 08
ffff8b940f2ad840 613986866 S Bo:3:009:1 -115 131 = 61538000 01020304 [...]
ffff8b945ae77780 613993087 C Bi:3:009:1 0 1 = 08
ffff8b945ae77780 614093552 S Bo:3:009:1 -115 256 = 6153fd00 01020304 [...]
ffff8b945ae77780 614105577 C Bi:3:009:1 0 1 = 08

First command sent is 61 53 01 00, which matches exactly one-byte write to 0x53 with payload 0x00. Response is 0x08 (I²C experienced successful transfer).

Second command is two-byte write to the same address, with payload 0x00 0x01, again successful.

Third command (the buggy one) is 61 53 00, in other words:

Response to it is, again, success.

All is well.

Ditto for the following commands, where usbmon doesn’t show the full payloads, but you can guess based on size that they’re correct.

OK, seabreeze isn’t at fault.

I²C bus: first three writes

Let’s take a look at the I²C bus. For this I’m using the visualizer I mentioned in the previous post.

i2c traffic showing the bug Visualized I²C traffic1 – first three transactions

In case you’re wondering, the third transaction is a 256-byte long write that (in this case) hits 0xe3 0x01 address, and the contents of it are partly leftovers from previous buffers2, partly zeroes.

So, there you have it, zero-length write request causes 256-byte long I²C write.

I²C bus: oversized writes

And because I’m a curious cat, let’s see what happens if you break spec and send 64, 128, or 253 bytes to be written:

i2c traffic showing oversized txn Visualized I²C traffic – transactions 4-6 (64, 128, 253 bytes)

The 64-byte write looks almost correct. Look at the byte 3 from the back, though. It’s 0x61 instead of the (correct) 0x3d. They weren’t kidding with the 61-byte limit :-D

The longer txns are funnier still3.

What spectrometers are affected?

I have tested:

I suspect this is universal to all the Ocean Optics spectrometers, rendering scanning for I²C devices impossible (or, at least, very unsafe).

Conclusion

I think this conclusively shows that there is a bug in Ocean Optics firmware that’s quite dangerous (if you’re naïve like me and write an I²C device scanner).

If you have a contact to relevant Ocean Optics devs, please pass this along. I’d love to have it fixed5.

  1. If you want the full log.csv so you can analyze it yourself, or the full viz, have at it.

  2. I haven’t run the command just once before taking a screenshot, y’know?

  3. Oh, and btw, if you try and write more than 253 bytes, the command stalls. I did not debug that further; felt no need.

  4. The DD4PA40MA1 and DD4BA40WA connectors are unobtainium, and so far I haven’t managed to get the official “breakout” board (FLAME-DD4-BREAKOUT-BOARD).

  5. Even though I’m done with I²C shenanigans when it comes to Ocean Optics spectrometers. :)