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 N → cn;
- backend B allocates token N for
sn and overwrites row N → sn.
Both keep serving from their in-memory map, so nothing is visible until a restart. After it, load() (PersistentCompressedSchema.java:139-186) reads N → sn 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.
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
Both are
static finalwith a literal in the baseDN slot. Every other tree of a backend is named fromEntryContainer.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:
sha224("compressed_schema/compressed_attributes")andsha224("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:
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:
There is one
PersistentCompressedSchemaperRootContainer, i.e. per backend (RootContainer.java:140), andexclusiveLockguards 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:cnand writes row N →cn;snand overwrites row N →sn.Both keep serving from their in-memory map, so nothing is visible until a restart. After it,
load()(PersistentCompressedSchema.java:139-186) reads N →snfor 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:
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:860already renamescompressed_schematrees, so the shape exists.getTableNameneeds no change once the tree name is distinct.