Skip to content

JDBC backends sharing a database URL share one pair of compressed-schema tables #873

Description

@vharseko

Every JDBC backend maps the compressed-schema trees to the same two tables, whatever its backend id or base DN. Two backends pointed at one database URL therefore share the token space of PersistentCompressedSchema, and the tokens are allocated per instance, so they can hand out the same token for different attribute descriptions and overwrite each other's definitions. Entries already written by the loser then decode with the winner's attribute description.

Surfaced while reviewing #867; it does not belong to that PR.

The tree names carry no backend qualifier

// opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/PersistentCompressedSchema.java:59
private static final TreeName adTreeName = new TreeName("compressed_schema", DB_NAME_AD);
private static final TreeName ocTreeName = new TreeName("compressed_schema", DB_NAME_OC);

Both are static final with a literal in the baseDN slot. Every other tree of a backend is named from EntryContainer.getTreePrefix(), which is derived from its base DN, so those differ per backend; these two do not.

The JDBC table name is a pure function of that tree name:

// opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java:157
final LoadingCache<TreeName,String> tree2table = Caffeine.newBuilder()
    .build(treeName -> { ... md.digest(treeName.toString().getBytes()) ... return "opendj_" + hashtext; });

sha224("compressed_schema/compressed_attributes") and sha224("compressed_schema/compressed_object_classes") are constants, so the two table names are constants too — identical for every JDBC backend in every server that shares the database.

That two backends really do share a database is not hypothetical; the test suite already works around it:

// opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java
/**
 * Backend test classes sharing one database map the same tree names to the same tables,
 * so a previous run may leave trees behind - including entries encrypted with a lost cipher key.
 */
static void dropStaleTrees(Connection con) throws SQLException {

Why sharing them corrupts data

The token is allocated from the size of the instance's own decode map, under the instance's own lock:

// opendj-server-legacy/src/main/java/org/opends/server/api/CompressedSchema.java:296
final Mappings mappings = this.mappings;
id = mappings.adEncodeMap.get(ad);
if (id == null)
{
  id = mappings.adDecodeMap.size();
  mappings.adDecodeMap.add(ad);
  mappings.adEncodeMap.put(ad, id);
  storeAttribute(encodeId(id), ad.getAttributeType().getNameOrOID(), ad.getOptions());
}

There is one PersistentCompressedSchema per RootContainer, i.e. per backend (RootContainer.java:140), and exclusiveLock guards that instance alone. Two backends that loaded the same table at open therefore have the same next token, and the first attribute description each encodes afterwards takes it:

  • backend A allocates token N for cn and writes row Ncn;
  • backend B allocates token N for sn and overwrites row Nsn.

Both keep serving from their in-memory map, so nothing is visible until a restart. After it, load() (PersistentCompressedSchema.java:139-186) reads Nsn for both, and every entry backend A wrote with token N decodes with the wrong attribute description. The same applies to the object class sets.

Not affected

The Cassandra backend qualifies its table with the backend id, so two Cassandra backends on one keyspace do not collide:

// opendj-server-legacy/src/main/java/org/opends/server/backends/cassandra/CASStorage.java:164
String getTableName() {
    return getKeyspaceName()+".\""+config.getBackendId().replaceAll("[^a-zA-Z0-9_]", "_")+"\"";
}

PDB and JE keep each backend in its own files, so the constant tree name is harmless there — which is presumably why it was never noticed.

Suggested fix

Qualify the two tree names with the same prefix the entry containers use, so they become per-backend, and give the upgrade tooling a rename step — UpgradeTasks.java:860 already renames compressed_schema trees, so the shape exists. getTableName needs no change once the tree name is distinct.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugdata-lossData integrity / loss of entriesjdbc

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions