Skip to content

A corrupt compressed schema token takes the backend down at open, or hangs it there #897

Description

@vharseko

A corrupt compressed schema token takes the backend down at open, or hangs it there

CompressedSchema.decodeId() folds the bytes of a token with no length cap and returns id - 1,
and the two load entry points hand that id straight to the decode map with neither a sign check
nor an upper bound:

// CompressedSchema.java, decodeId()
int id = 0;
for (final byte b : idBytes)
{
  id <<= 8;
  id |= b & 0xFF;
}
return id - 1; // Subtract 1 to compensate for old behavior.
// CompressedSchema.java, loadAttributeToMaps() - loadObjectClassesToMaps()/updateObjectClassesMaps()
// has the same shape
mappings.adEncodeMap.put(ad, id);
if (id < mappings.adDecodeMap.size())
{
  mappings.adDecodeMap.set(id, ad);
}
else
{
  // Grow the decode array.
  while (id > mappings.adDecodeMap.size())
  {
    mappings.adDecodeMap.add(null);
  }
  mappings.adDecodeMap.add(ad);
}

Both are reached from loadAttribute()/loadObjectClasses(), the SPI an implementation calls once
per record it read out of its storage - PersistentCompressedSchema.load() while cursoring the
compressed_attributes/compressed_objectclasses trees, DefaultCompressedSchema.load() while
reading config/compressedschema.dat. Neither validates the key it read.

Two keys are enough:

  • An all-zero key gives id == -1. The guard id < size() passes, and set(-1, ad) throws
    IndexOutOfBoundsException. Nothing on the load path catches it as anything but a fatal error:
    it leaves DefaultCompressedSchema.load() as throw new RuntimeException(e), and
    PersistentCompressedSchema's constructor runs inside RootContainer.open(), so it surfaces as a
    StorageRuntimeException. The backend does not open.

  • A key of 0x7FFFFFFF gives id == 2147483646, and the else branch pads the decode map up to
    it one element at a time. The map is a CopyOnWriteArrayList, so every add copies the whole
    backing array - two billion appends, quadratic, under exclusiveLock, and for the pluggable
    backend inside RootContainer.open()'s write transaction. The backend never finishes opening and
    nothing in the log says why.

A four-byte key is what the encoder itself writes past id 16777214, so neither value needs a hostile
store - a truncated or partially written record is enough.

Suggested fix

Validate in loadAttribute()/loadObjectClasses(), before anything reaches the maps:

  • reject id < 0;
  • reject an id beyond a sane ceiling for a compressed schema (the number of records the caller is
    loading is an upper bound the SPI could take, or a fixed cap well below Integer.MAX_VALUE);
  • report the rejected token as the key the storage holds - CompressedSchema.tokenInMessage()
    already formats one that way for the decode path - and skip the record rather than fail the open,
    so one unreadable definition leaves a gap the decode path already handles instead of taking the
    whole backend down.

Notes

Pre-existing, and untouched by #894 - raised there while reviewing the sibling read path, which
#894 hardened with decodeMapGet(). decodeMapGet() covers the decode side (an entry carrying a
token with no definition is now reported as an unknown token); this is the load side, which still
has no bound at all.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions