Page MenuHome GnuPG

keyring / keybox race
Closed, ResolvedPublic

Description

Consider how a keyblock is updated when using a keyring:

  hd = keydb_new ();
  keydb_search (hd, ...);
    hd->found.offset = offset of matching keyblock

  keydb_update_keyblock (hd, keyblock);
    lock_all (hd);
    keyring_update_keyblock (hd, keyblock);
      replace block starting at hd->found.offset with the new keyblock
    unlock_all (hd);

The main problem is that we have a time of check to time of use (TOCTTOU) bug:
we don't look up the offset while we have the lock so another process could have
come along and modified the DB causing us to update the wrong block.

This problem could be fixed by changing keydb_update_keyblock to take a search
description of the keyblock to update. Then it can locate and replace the
record without dropping the lock.

The same basic problem exists for keydb_insert_keyblock. In
import.c:import_one, we check whether the key is in the keyring. If not, then
we insert it using keydb_insert_keyblock. We can fix this by adding a parameter
may_insert to our new keydb_update_keyblock function to indicate whether it is
acceptable to add a new keyblock if one doesn't already exist.

Event Timeline

Note: there is also a TOCTTOU bug for keydb_search / keydb_get_keyblock.

Note the corruption that occurs is rather subtle. It occurs silently, because
copy_some_packets doesn't throw an error if the next packet to process doesn't
start on STOPOFF, but continues until the offset of the next packet to process
is at *or exceeds* STOPOFF.

Imagine that we have a keyblock A at offset 0 and a second keyblock B at offset
100 with 2 packets:

  • A
  • B
  • The first gpg process does a search for the key at offset 100
  • A second process looks up and updates the key block (A') at offset 0 such

that it now has a length of 150 and 4 packets after offset 100.

  1. The initial process "updates" B to B'. hd->found.offset now point into the

middle of A'. In keyring.c:do_copy, the first 100 bytes plus any bytes required
to complete the last packet are copied (by copy_some_packets). The next 2
packets are deleted (skip_some_packets) and the new keyblock is inserted. We
now have the following:

  • 100+ bytes of A'
  • B'
  • Last two packets of A'
  • B

And B appears to be duplicated.

I was aware of that problem but always wondered why I never noticed such a case.
Your analysis is correct and explains the problem. The locking of the keyblock
does not help here (it was introduced only a few years ago).

Instead of making use of found.offset and fix that with your suggested trick we
should not use the offset at all but let the update and insert functions handle
it atomicly - this may result in an insert/update error (e.g. if another process
inserted/deleted the key) but that is an expected outcome if two processes
manipulate the same key.

This should not be fixed for the old keyring format but only for the keybox format:

  1. The keyring format is deprecated
  2. This introduces a new behaviour and may raise other problems.

If you want to fix that, please do that in a new branch.

atomicly here mean that the update/insert functions locate an possibly existing
key using the fingerprint while holding the lock.

Anyway, to really fix that we need a daemon taking control of all keys - a task
for 2.3,

My proposed solution is to change keydb_update_keyblock. We don't actually need
to touch the keybox or keyring code.

By the new behavior, I guess you mean getting an error when deleting a key, but
it fails because another process already deleted it. If something like this
were to current occur, then we'd end up with silent corruption. So, it's not
clear how this new behavior would introduce new behavior that could raise problems.

I've attached a fix that does a very small and straightforward modification to
keydb_update_keyblock, which fixes this problem for both the keyring and keybox.

Neal, what is the status of this bug?

werner removed a project: Restricted Project.