PostgreSQL Index Only Scan can NOT return ctid
Your understanding is almost but not quite correct. True, every btree index tuple needs a ctid (or some form of block number and tuple index) to point to the heap tuple (table row). But (at least) since the introduction of "heap-only tuples" with Postgres 8.3, there may be a HOT chain to follow to arrive at the current live tuple in the snapshot - with a different ctid than the one stored in the index.

Since it is also not possible to use the ctid (or any system column) as index expression, it is currently (and for the forseeable future) completely impossible to get a ctid from an index-only scan.

Related:


Redundant data in update statements
How do I decompose ctid into page and row numbers?


Quoting the Postgres Wiki on Index-only_scans / Interaction_with_HOT:


  With HOT, it became possible for an index scan to traverse a so-called
  HOT chain; it could get from the physical index tuple (which would
  probably have been created by an original INSERT, and related to an
  earlier version of the logical tuple), to the corresponding physical
  heap tuple. The heap tuple would itself contain a pointer to the next
  version of the tuple (that is, the tuple ctid), which might, in turn,
  have a pointer of its own. The index scan eventually arrives at tuple
  that is current according to the query's snapshot.