Schizophrenic floorplans

Introduction

Before a chip is manufactured, its design is flattened into a layout file: a long list of shapes, each on a particular layer, describing exactly what will be printed onto the silicon. Many different tools read this file along the way. Some check it against design rules, some render it on screen for a human to inspect, and eventually one turns it into masks. Everyone involved assumes that they are all looking at the same layout.

But no format is perfect. In this post I'll show how to craft a layout file that looks like one chip to one program and a different chip to another, without either program noticing anything wrong.

The OASIS file format

OASIS (Open Artwork System Interchange Standard) is a binary file format for specifying integrated circuit layout data. It’s the successor to GDSII. GDSII was the industry standard for some decades, but it suffered various limitations. Notably, it didn’t scale well in the sense that its fixed-width fields and lack of compression made GDSII files explode in size for our increasingly complex layouts.

OASIS is an all-round better format. It achieves smaller file sizes through a combination of variable-length encoding, modal (stateful) fields, repetition structures, and compression.

Here’s a crash course of how it works. An OASIS file is a flat sequence of so-called records. There are many different record types, but some relevant ones are:

Type Name Description
1 START File header. Contains version, length unit convention, and some optional metadata.
2 END File footer. Contains an optional validation signature or checksum.
3 CELLNAME Declares a cell name.
11 LAYERNAME Associates a human-readable name with a range of layer/datatype numbers.
13 CELL Begins a cell definition. All subsequent records involving shapes or placements belong to this cell, until the next CELL or END record.
17 PLACEMENT Places a cell in another cell.
20 RECTANGLE A rectangle.
21 POLYGON A polygon.
34 CBLOCK A DEFLATE-compressed block wrapping a sequence of other records.

Each record contains descriptive data about the object or thing it’s encoding. For example, a RECTANGLE record consists of the following components:

Component Datatype Meaning
'20' Unsigned integer Record ID
rectangle-info-byte Byte See below
layer-number Unsigned integer Optional; layer number that rectangle belongs to
datatype-number Unsigned integer Optional; datatype number that rectangle belongs to
width Unsigned integer Optional; width of rectangle
height Unsigned integer Optional; height of rectangle
x Signed integer Optional; X-position of rectangle
y Signed integer Optional; Y-position of rectangle
repetition Repetition Optional; whether the rectangle should be repeated, e.g. in an array

The rectangle-info-byte contains the bit pattern SWHXYRDL, where the individual bits have the following meaning.

Bit Meaning
S Whether the rectangle is square. If yes, H must be zero and width can be used for both dimensions.
W Whether width is present.
H Whether height is present.
X Whether x is present.
Y Whether y is present.
R Whether repetition is present.
D Whether datatype-number is present.
L Whether layer-number is present.

If a field is not present, use the value that was used for the previous record. So if the previous RECTANGLE record belonged to layer number 3, and this RECTANGLE record has no listed layer-number, then the rectangle belongs to layer 3.

Let’s look at an OASIS file and see what it looks like at a byte level.

00000000: 2553 454d 492d 4f41 5349 530d 0a01 0331  %SEMI-OASIS....1
00000010: 2e30 00e8 0700 0000 0000 0000 0000 0000  .0..............
00000020: 0000 0b02 4d31 0301 0300 0303 544f 500d  ....M1......TOP.
00000030: 0014 7b01 0064 3200 0014 7864 3290 0300  ..{..d2...xd2...
00000040: 02fc 0100 0000 …  ......…

I’ve colour-coded the regions of the OASIS file.

Colour Region Bytes
Grey Magic bytes 0x00–0x0c
Blue START 0x0d–0x21
Green LAYERNAME 0x22–0x29
Cyan CELLNAME 0x2a–0x2e
Red CELL 0x2f–0x30
Yellow First RECTANGLE 0x31–0x38
Pink Second RECTANGLE 0x39–0x3f
Gray END 0x40–0x13f

Now let’s go through the regions one by one.

Byte(s) Meaning
25 53 45 4d 49 2d 4f 41 53 49 53 0d 0a Magic bytes. ASCII text "%SEMI-OASIS", followed by a carriage return and line feed.
01 Record ID 1: START.
03 31 2e 30 Version string: length 3, then "1.0".
00 e8 07 1000 database units per micron.
00 Offset flag 0. Table of offsets follow right here.
00 00 00 00 00 00 00 00 00 00 00 00 Table of zeros, meaning ‘no tables’.
0b Record ID 11: LAYERNAME.
02 4d 31 Name: length 2, then "M1".
03 01 Layer number 1.
03 00 Datatype number 0.
03 Record ID 3: CELLNAME.
03 54 4f 50 Name: length 3, then "TOP".
0d Record ID 13: CELL.
00 Name of cell. Look at first CELLNAME record (we start counting at 0). So this is cell "TOP".
14 Record ID 20: RECTANGLE.
7b Info byte 01111011. Not a square; width, height, x and y present; no repetition; datatype and layer present.
01 Layer number 1.
00 Datatype number 0.
64 Width 100.
32 Height 50.
00 00 Position (0, 0).
14 Record ID 20: RECTANGLE.
78 Info byte 0111 1000: width, height, x and y present; datatype and layer absent, so they are the same as before.
64 Width 100.
32 Height 50.
90 03 00 Position (200, 0).
02 Record ID 2: END.
fc 01 Length of (mandatory) padding string: 252.
00 … 00 252 bytes of padding.
00 Validation scheme 0: no checksum.

What could go wrong?

The OASIS spec is surprisingly precise, and it usually states explicitly whether or not certain anomalous behaviour should result in an error. For instance, the spec explicitly says that the appearance of two CELLNAME records with the same name “should be treated as a fatal error.”Note 1 Despite this, there are many ways in which ambiguous behaviour could be introduced into an OASIS file.

I thought of a bunch of ways to break an OASIS file or make it ambiguous, then tried loading each one in KLayout and gdstk, two common programs for reading OASIS files. The results are below.

Trick KLayout Gdstk
List a position twice in a repetition instance ⚠️ Removes duplicate ✅ Keeps them
Rotation of 450° ⚠️ Warns, reads it as 90° ✅ Reads it as 450°
Property refers to an undefined name ❌ Error: No property name defined ❌ Segfault
Unicode cell name ✅ Allows it ✅ Allows it
Empty cell name ❌ Error: Some cell IDs don't have a name ✅ Allows it
Two names for one layer ⚠️ Combines them Doesn't support layer names
Layer number 232 + 1 ❌ Error: uin32 value overflow ⚠️ Silently overflows to 1
Layer number 264 + 1 ⚠️ Silently overflows to 1 ❌ Segfault
Unit NaN ⚠️ Accepts it ⚠️ Accepts it; empty bounding boxes
Unit ∞ ⚠️ Accepts it; dbu becomes zero ⚠️ Reads it with a precision of 0
Place cell into itself ❌ Error: Internal assertion topological_sort() ⚠️ Accepts it, but boundary box segfaults
Circle of radius 0 ⚠️ Drops it ✅ Accepts it

As you can see, it is easy to come up with undefined behaviour. In some cases, the trick is an explicit violation of the spec. For instance, cell names are only allowed to have a restricted set of ASCII characters. Yet both KLayout and gdstk decide to allow effectively any Unicode string, presumably because it is within a user’s interest to be as lenient as possible when parsing files, and only allowing some ASCII characters is arguably too harsh a restriction.

The most salient ambiguity

There’s one trick I left out of the table, and it deserves special attention because of its implications. This trick involves a special kind of record, called the CBLOCK record.

CBLOCK records allow you to embed compressed data within an OASIS file. It has the following format.

Component Datatype Meaning
'34' Unsigned integer Record ID
comp-type Unsigned integer Type of compression
uncomp-byte-count Unsigned integer Number of bytes prior to compression
comp-byte-count Unsigned integer Number of bytes after compression
comp-bytes Bytes Sequence of bytes containing the compressed byte sequence

When comp-type is set to 0, the compression scheme is the lossless DEFLATE compression format. When the spec was made, other values were reserved for future versions of the OASIS format, but no future version was ever released,Note 2 so in practice, this number is always set to 0.

DEFLATE is a lossless compression format used for instance in gzip, PNG, and over HTTP. It combines two compression algorithms:

  1. LZ77: Removes repetition.
  2. Huffman coding: Shrinks the symbols.

We don’t need to go through the details of DEFLATE, but there’s one seemingly innocent detail about it that I want you to know of:

DEFLATE produces so-called block headers. The first bit of every block header is called BFINAL, and when it’s 1, that block is the last one of the stream. This allows the decoder to know when to stop.

Why is that relevant? When an OASIS parser encounters a CBLOCK record, the parser needs to spawn a decoder that decompresses a stream of DEFLATE-compressed bytes. This decoder should continue until it encounters a BFINAL value of 1, at which point it stops, and the main OASIS parser continues reading the file.

Since our decoder knows when to stop, the comp-byte-count value specified in CBLOCK is actually redundant, and a parser could, in principle, ignore it. And this is where a fundamental ambiguity arises. If we set comp-byte-count to a wrong value, two things can happen, and both occur in practice.

  1. After decompressing the DEFLATE stream, the parser continues comp-byte-count bytes after the start of the stream. This is the path taken by gdstk, as well as by ASML Brion’s Tachyon platform.
  2. After decompressing the DEFLATE stream, the parser continues wherever the decoder ended, ignoring comp-byte-count. KLayout does this.

To illustrate how this can be used, consider the following snippet of bytes:

22 00 01 0a                CBLOCK with comp-byte-count 10
63 00 00                   Empty DEFLATE stream (3 bytes)
14 db 01 00 64 00 00       RECTANGLE (7 bytes)

Here’s how the two scenarios play out.

  1. The parser encounters the CBLOCK and decompresses the DEFLATE stream, which contains nothing.Note 3 It then continues 10 bytes after the start of the stream, as suggested by comp-byte-count, skipping over the RECTANGLE.
  2. The parser encounters the CBLOCK, decompresses the DEFLATE stream, then continues immediately after the stream, ignoring comp-byte-count, thereby encountering the hidden RECTANGLE.

With this method, we are able to inject part of a layout that is visible to KLayout, yet invisible to other software.

The reverse is possible, too! To pull this off, we need to use another record, called the XELEMENT record. To my understanding, this record was intended to allow some backward compatibility, though I have never seen it used in the wild, and in practice, all parsers simply ignore this record.

The record has the following format.

Component Datatype Meaning
'32' Unsigned integer Record ID
xelement-attribute Unsigned integer Provide ability to associate XELEMENT with user-defined class
xelement-string String Any user-defined data

Since XELEMENT records are ignored in practice, we can inject a payload into its xelement-string field encoding a hidden part of the layout, and it will only be read if the parser is unaware that it is sitting in an XELEMENT record.

Here’s how that looks in practice.

22 00 01 06                CBLOCK with comp-byte-count 6
63 00 00                   Empty DEFLATE stream (3 bytes)
20 00 07                   Start of XELEMENT record: incoming string of length 7
14 db 01 00 64 00 00       RECTANGLE (7 bytes)

The two scenarios are now as follows.

  1. The parser encounters the CBLOCK and decompresses the DEFLATE stream, which contains nothing. It then continues 6 bytes after the start of the stream, as suggested by comp-byte-count, ending up at what it thinks is a RECTANGLE record.
  2. The parser encounters the CBLOCK, decompresses the DEFLATE stream, then continues immediately after the stream, ignoring comp-byte-count. It encounters an XELEMENT record, which it ignores.

It stands to reason that you can arbitrarily extend this setup, so that two parsers will end up seeing two altogether different layouts.

Conclusion

Nothing here required breaking the OASIS format. Every file in this post is a valid sequence of records, and each parser did something defensible with it. The trouble is that the spec leaves room for a redundant length field to disagree with the data it describes, and parsers filled that gap in different ways. Two sensible readings of the same bytes were enough to give two different chips.

The remedy for this particular trick is simple: if the length of the DEFLATE stream differs from comp-byte-count, refuse to parse the file.

The deeper cause is leniency. It is understandable that parsers try to read whatever they are given: nobody wants a design rejected over a technicality. KLayout in particular goes to great lengths to recover from malformed input, and it often ignores headers and flags in favour of a best-effort parse. But for a file that several tools must agree on, every point where a parser quietly picks an interpretation is a point where two parsers can pick differently. The same goes for the optional checksum, which most major parsers ignore.

In a flow where a layout is checked by one tool and manufactured from another, that disagreement is a security hole, not a curiosity. If the format is ambiguous, the only safe response is to reject the file. A layout parser should be strict about what it accepts, even if that occasionally annoys a designer.

Footnotes

  1. Despite this, many OASIS parsers do not, in fact, treat this as a fatal error. KLayout renames one cell by appending $1 to the name. Gdstk allows it without complaining.
  2. An extension to the OASIS format was later introduced, supporting new record types with curvilinear features. For some reason, it was decided not to simply increment the version. Instead, this file format comes with a new set of magic bytes, making it backward-incompatible with the standard OASIS spec.
  3. Actually, it contains a single literal 0x00 byte. 0x00 is called a PAD record, and OASIS parsers are configured to skip it.