trustdb locking

NIIBE Yutaka gniibe at fsij.org
Mon Jun 13 04:34:52 CEST 2016


Hello,

We have this bug in 1.4, 2.0, and 2.1.  My intention is to fix
in 2.1 and backporting fix to 1.4 and 2.0.

On 06/09/2016 05:12 PM, NIIBE Yutaka wrote:
> In the issue 1675, we handle trustdb locking:
>     https://bugs.gnupg.org/gnupg/issue1675
> 
> I had identified a race condition for creation of trustdb.gpg.  This
> was fixed last year.  However, the problem of trustdb corruption has
> not gone yet.
[...]
> (2) BEING ANALYZED
>     The serialization of newly creating hash table in the function
>     create_hashtable (tdbio.c).  <--- I think this is the issue now.
> 
>     When two processes race for the position of end of file by lseek
>     (db_fd, 0, SEEK_END), it might result corrupted trustdb.  A
>     process which comes later will also create a record for hash table
>     at the end of file at later time, but the block will be
>     overwritten by another process which comes first.

While this is a race condition (WRITE vs. WRITE), I think I finally
identified another race condition (WRITE vs. READ) which can reproduce
same result of issue1675.

In 2.1, I managed to reproduce it by following steps.

(0) We will use the file SHA256SUMS.gpg and SHA256SUMS to verify the
    signature as the report of issue1675 does.  Get the files from:

http://archive.ubuntu.com/ubuntu/dists/precise/main/installer-amd64/current/images
    (Any signature works, here we use same files as the report)

(1) Make a temporal homedir environment.

    $ mkdir /tmp/gpghome; chmod og-rwx /tmp/gpghome

(2) Prepare a key for verification (any key works, here we use same
    key as the report).

    $ gpg2-1-12 --homedir=/tmp/gpghome --keyserver=sks-keyservers.net
--recv-key 40976EAF437D05B5

    Then, we have pubring.kbx and trustdb.gpg of 1200-byte.
    Here, trustdb.gpg is composed by version record and hash table.

    To reproduce the bug, we setup artificial situation.

    $ rm /tmp/gpghome/trustdb.gpg
    $ mv /tmp/gpghome/pubring.kbx /tmp/gpghome/pubring.kbx.bak

(3) Let GPG make a VERSION record-only of trustdb.gpg by -k command
    with no-key.

    $ gpg2-1-12 --homedir=/tmp/gpghome -k

    Then, restore the public key

    $ mv  /tmp/gpghome/pubring.kbx.bak /tmp/gpghome/pubring.kbx

(4) Now, it has 40-byte trustdb.gpg, which is VERSION record only.
    ls -l /tmp/gpghome shows like:

    -rw-r--r-- 1 gniibe gniibe 10059 Jun 13 11:04 pubring.kbx
    -rw------- 1 gniibe gniibe    40 Jun 13 11:06 trustdb.gpg

(5) In this situation invoke GPG under GDB.

    $ gdb /path-to/gpg2-1-12

    Then, let GDB have a break point at write_cache_item, and run GPG.

    (gdb) break write_cache_item
    (gdb) run --homedir=/tmp/gpghome --verify SHA256SUMS.gpg SHA256SUMS

    Then, we see:

    Breakpoint 1, write_cache_item (r=r at entry=0xbf380)
        at ../../gnupg/g10/tdbio.c:201
    201	  if (lseek (db_fd, r->recno * TRUST_RECORD_LEN, SEEK_SET) == -1)
    (gdb) list
    196	write_cache_item (CACHE_CTRL r)
    197	{
    198	  gpg_error_t err;
    199	  int n;
    200	
    201	  if (lseek (db_fd, r->recno * TRUST_RECORD_LEN, SEEK_SET) == -1)
    202	    {
    203	      err = gpg_error_from_syserror ();
    204	      log_error (_("trustdb rec %lu: lseek failed: %s\n"),
    205	                 r->recno, strerror (errno));
    (gdb) print *r
    $67 = {next = 0xbf348, flags = {used = 1, dirty = 1}, recno = 0,
      data = "\001gpg\003\003\001\005\001\002\000\000W^\025\032", '\000'
<repeats 23 times>, "\001"}

    The version record is now updating.  Let GDB continue the execution
    of GPG.

    (gdb) cont
    Continuing.

    Breakpoint 1, write_cache_item (r=r at entry=0xbf348)
        at ../../gnupg/g10/tdbio.c:201
    201	  if (lseek (db_fd, r->recno * TRUST_RECORD_LEN, SEEK_SET) == -1)
    (gdb) print *r
    $69 = {next = 0xbf310, flags = {used = 1, dirty = 1}, recno = 29,
data = "\n", '\000' <repeats 38 times>}

    After the update of the version record, now GPG is writing to the
    hash table.

    Note that the order of writing data to disk.  It updates the
    version record first, then, hash table.  When let it go further
    step, it will be more clear.

    (gdb) cont
    Continuing.

    Breakpoint 3, write_cache_item (r=r at entry=0xbf310)
        at ../../gnupg/g10/tdbio.c:201
    201	  if (lseek (db_fd, r->recno * TRUST_RECORD_LEN, SEEK_SET) == -1)
    (gdb) print *r
    $71 = {next = 0xc7f00, flags = {used = 1, dirty = 1}, recno = 28,
data = "\n", '\000' <repeats 38 times>}


    The RECNO is, 0 for the version record, then 1...29 for the hash
    table.  It goes from 29 to 1.

    Here (after the write to the version record), we keep stopping the
    GPG process, and we invoke another GPG command.

(6) In another terminal, invoke GPG, then, we got the error.

    $ gpg2-1-12 --homedir=/tmp/gpghome --verify SHA256SUMS.gpg SHA256SUMS
    gpg: Signature made Tue 24 Apr 2012 04:52:09 AM JST using DSA key ID
437D05B5
    gpg: 12: read expected rec type 10, got 0
    gpg: lookup_hashtable failed: Trust DB error
    gpg: trustdb: searching trust record failed: Trust DB error
    gpg: Error: The trustdb is corrupted.
    gpg: You may try to re-create the trustdb using the commands:
    gpg:   cd ~/.gnupg
    gpg:   gpg --export-ownertrust > otrust.tmp
    gpg:   rm trustdb.gpg
    gpg:   gpg --import-ownertrust < otrust.tmp
    gpg: If that does not work, please consult the manual


It fails because the hash table is currently being written by the GPG
process under GDB (holding the write lock).

--



More information about the Gnupg-devel mailing list