view doc/TIFFS-old-description @ 792:44e034f21916

fc-simint: brown paper bag
author Mychaela Falconia <falcon@freecalypso.org>
date Fri, 19 Mar 2021 04:47:32 +0000
parents e7502631a0f9
children
line wrap: on
line source

The description of TIFFS that follows was originally written in the summer of
SE52 (A.D. 2013), before the major TI source discoveries which happened later
that year.  The text following the dividing line below has not been edited in
content since it was written; for a newer write-up based on the current source-
enabled understanding and reflecting the current FreeCalypso plans with respect
to this FFS, see TIFFS-Overview.

-------------------------------------------------------------------------------

This is a description, based on reverse engineering, of the flash file system
(FFS) implemented in Pirelli's original firmware for the DP-L10 GSM/WiFi dual
mode mobile phone, and in the Closedmoko GTA0x modem firmware.  Not knowing the
"proper" name for this FFS, and needing _some_ identifier to refer to it, I
have named it Mokopir-FFS, from "Moko" and "Pirelli" - sometimes abbreviated
further to MPFFS.

(I have previously called the FFS in question MysteryFFS; but now that I've
 successfully reverse-engineered it, it isn't as much of a mystery any more :-)

At a high functional level, Mokopir-FFS presents the following features:

* Has a directory tree structure like UNIX file systems;

* The file system API that must be implemented inside the proprietary firmware
  appears to use UNIX-style pathnames; doing strings on firmware images reveals
  pathname strings like these:

  /var/dbg/dar
  /gsm/l3/rr_white_list
  /gsm/l3/rr_medium_rxlev_thr
  /gsm/l3/rr_upper_rxlev_thr
  /gsm/l3/shield

  Parsing the corresponding FFS image with tools included in the present
  package has confirmed that the directory structure implied by these pathnames
  does indeed exist in the FFS.

* Absolutely no DOS-ish semantics seen anywhere: no 8.3 filenames and no
  colon-separated device names (seen in the TSM30 file system source, for
  example) are visible in the Closedmoko/Pirelli FFS.

* File contents are stored uncompressed, but not necessarily contiguous: one
  could probably store a file in FFS which is bigger than the flash sector
  size, it which case it can never be contiguous in a writable FFS (see below),
  and the firmware implementation seems to limit chunk sizes to a fairly small
  number: on the Pirelli phones all largish files are divided into chunks of
  8 KiB each, and on my GTA02 the largest observed chunk size is only 2 KiB.

  The smaller files, like the IMEI and the firmware ID strings in my GTA02 FFS,
  are contiguous.

* The FFS structure is such that the length of "user" payload data stored in
  each chunk (and consequently, in each file) can be known exactly in bytes,
  with the files/chunks able to contain arbitrary binary data.  (This property
  may seem obvious or trivial, as all familiar UNIX and DOS file systems have
  it, but contrast with RT-11 for example.)

* The flash file system is a writable one: the running firmware can create,
  delete and overwrite files (and possibly directories too) in the live FFS;
  thus the FFS design is such that allows these operations to be performed
  within the physical constraints of NOR flash write operations.

I have reverse-engineered this Mokopir-FFS on a read-only level.  What it means
is that I, or anyone else who can read this document and the accompanying
source for the listing/extraction utilities, can take a Mokopir-FFS image read
out of a device and see/extract its full content: the complete directory tree
and the exact binary byte content of all files contained therein.

However, the knowledge possessed by the present hacker (and conveyed in this
document and the accompanying source code) is NOT sufficient for constructing a
valid Mokopir-FFS image "in vitro" given a tree of directories and files, or
for making modifications to the file or directory content of an existing image
and producing a content-modified image that is also valid; valid as in suitable
for the original proprietary firmware to make its normal read and write
operations without noticing anything amiss.

Constructing "de novo" Mokopir-FFS images or modifying existing images in such
a way that they remain 100% valid for all read and write operations of the
original proprietary firmware would, at the very minimum, require an
understanding of the meaning of *all* fields of the on-media FFS format.  Some
of these fields are still left as "non-understood" for now though: a read-only
implementation can get away with simply ignoring them, but a writer/generator
would have to put *something* in those fields.

As you read the "read-only" description of the Mokopir-FFS on-media format in
the remainder of this document, it should become fairly obvious which pieces
are missing before our understanding of this FFS can be elevated to a
"writable" level.

However, when it comes to writing new code to run on the two Calypso phones in
question (Closedmoko and Pirelli), it seems, at least to the present hacker,
that a read-only understanding of Mokopir-FFS should be sufficient:

* In the case of Closedmoko GTA0x modems, the FFS is seen to contain the IMEI
  and the RF calibration data.  The format of the former is obvious; the latter
  not so much - but in any case, the information of interest is clearly of a
  read-only nature.  It's difficult to tell (or rather, I haven't bothered to
  experiment enough) whether the Closedmoko firmware does any writes to FFS or
  if the FFS is treated as read-only outside of the production line environment,
  but in any case, it seems to me that for any 3rd party replacement firmware,
  the best strategy would be to treat the FFS as a read-only source of IMEI and
  RF calibration data, and nothing more.

* In the case of Pirelli phones, the FFS is used to store user data: sent and
  received SMS (and MMS/email/whatever), call history, UI settings, pictures
  taken with the camera, and whatever else.  It also stores a ton of files
  which I can only presume were meant to be immutable except at the time of
  firmware updates: graphics for the UI, ringtones, i18n UI strings, and even
  "helper" firmware images for the WiFi and VoIP processors.  However, no IMEI
  or RF calibration data are anywhere to be found in the FFS - instead this
  information appears to be stored in the "factory block" at the end of the
  flash (in its own sector) outside of the FFS.

  Being able to parse FFS images extracted out of Pirelli phones "in vitro"
  allows us to steal some of these helper files (UI artwork, ringtones,
  WiFi/VoIP helpers), and some of these might even come useful to firmware
  replacement projects, but it seems to me that a replacement firmware would
  be better off using its own FFS design for storing user data, and as to
  retrieving the original IMEI and RF calibration data, the original FFS isn't
  of any use for that anyway.

=======================
Moko/Pirelli FFS format
=======================

OK, now that I'm done with the introduction, we can get to the actual
Mokopir-FFS format.

* On the GTA0x modem (or at least on my GTA02; my sample size is 1) the FFS
  occupies 7 flash sectors of 64 KiB each at offsets 0x380000 through 0x3E0000,
  inclusive.

(The 4 MiB NOR flash chip used by Closedmoko has an independent R/W bank
 division between the first 3 MiB and the last 1 MiB.  The first 3 MiB are used
 to hold the field-flashable closed firmware images distributed as *.m0 files;
 the independent last megabyte holds the FFS, and thus the FW could be
 implemented to do FFS writes while running from flash in the main bank.
 Less than half of that last megabyte appears to be used for the FFS though;
 the rest appears to be unused - blank flash observed.)

* On the Pirelli the FFS occupies 18 sectors of 256 KiB each at offsets 0
  through 0x440000 (inclusive) of the 2nd flash chip select, the one wired to
  nCS3 on the Calypso.

Each flash sector allocated to FFS begins with the following signature:

00000000:  46 66 73 23 10 02 xx yy  zz FF FF FF FF FF FF FF  Ffs#............

The bytes shown as xx and yy above serve a non-understood purpose; as a guess,
they may hold some info for the flash wear leveling algorithm: in a "virgin"
FFS image like that found in my GTA02 (which never had a SIM card in it and
never made or received a call) or read out of a "virgin" Pirelli phone that
hasn't seen any active use yet, both of these bytes are FFs, but when I look at
FFS images read out of the Pirelli which I currently use as my everyday-use
cellphone, I see other values in sectors which must have been erased and
rewritten.  A read-only implementation can ignore these bytes, as mine does.

The byte shown as zz is more important though, even to a read-only
implementation.  The 3 values I've encountered in this byte so far are AB, BD
and BF.  Per my current understanding, in a "healthy" FFS exactly one sector
will have AB in its header, exactly one will have BF, and the rest will have
BD.  The meanings are (or appear to be):

AB: the sector holds a vital data structure which I have called the active
    index block;
BD: the sector holds regular data;
BF: the sector is blank except for the header, can be turned into a new AB or
    BD.

(Note that a flash program operation, which can turn 1s into 0s but not the
 other way around, can turn BF into either AB or BD - but neither AB nor BD can
 be turned into any other valid value.)

In a "virgin" FFS image (as explained above) the first FFS sector is AB, the
last one is BF, and the ones in between are BDs.

An FFS read operation (a search for a given pathname, or a listing of all
present directories and files) needs to start with locating the active index
block - the FFS sector with AB in the header.  Following this header, which is
treated as being 16 bytes long (almost everything in Mokopir-FFS is aligned on
16-byte boundaries), the active index block contains a linear array of 16-byte
records, each record describing an FFS object: directory, file or file
continuation chunk.

Here is my current understanding of the 16-byte index block record structure:

2 bytes: Length of the described chunk in bytes
1 byte:	 Purpose/meaning not understood, ignored by my current code
1 byte:	 Object type
2 bytes: Descendant pointer
2 bytes: Sibling pointer
4 bytes: Data pointer
4 bytes: Purpose/meaning not understood, ignored by my current code

(On the Calypso phones of interest, all multibyte fields are in the native
 little-endian byte order of the ARM7TDMI processor.)

The active index block gets filled with these records as objects are created;
the first record goes right after the 'Ffs#'...AB header (padded to 16 bytes);
the last record (at any given moment) is followed by blank flash for the
remainder of the sector.  Records thus appear in the order in which they are
created, which bears no direct relation to the directory tree structure.

The objects, each described by a record in the index block, are organized into
a tree structure by the descendant and sibling pointers, plus the object type
indicator byte.  Let's start with the latter; the following objtype byte values
have been observed:

00: deleted object - a read-only implementation should ignore everything except
    the descendant and sibling pointers.  (A write-capable implementation would
    need more care - it would need a way of reclaiming dirty flash space taken
    up by deleted/overwritten files.)

E1: a special file - see the description of the /.journal file further down
F1: a regular file (head chunk thereof)
F2: a directory
F4: file continuation chunk (explained below)

Each record in the index block has an associated chunk in one of the data
sectors; the index record contains fields giving the address and length of this
chunk.  The length of a chunk is always a nonzero multiple of 16 bytes, and is
stored (as a number in bytes) in the first 16-bit field of the 16-byte index
entry.  The address of each chunk is given by the data pointer field of the
index record, and it is reckoned in 16-byte units (thereby 16-byte alignment is
required) from the beginning of the FFS sector group in the flash address space.

For objects of type F1 and F2 (regular files and directories) the just-described
chunk begins with the name of the file or subdirectory as a NUL-terminated ASCII
string.  This name is just for the current level of the directory tree, just
like in UNIX directories, thus one will have chunk names like gsm, l3, eplmn
etc, rather than /gsm/l3/eplmn.  One practical effect is that one can't readily
see pathnames or any of the directory structure by looking at an FFS image as a
raw hex dump; the structure is only revealed when one uses a parsing program
like those which accompany this document.

In the case of directories, the "chunk" part of the object contains only the
name of the directory itself, padded with FFs to a 16-byte boundary.  For
example, an FFS directory named /gsm would be represented by an object
consisting of two flash writes: a 16-byte entry in the active index block, with
the object type byte set to F2, and a corresponding 16-byte chunk in one of the
data sectors, with the 16 bytes containing "gsm", a terminating NUL byte, and
12 FF bytes to pad up to 16.  In the case of files, this name may be followed
by the first chunk of file data content, as explained further down.

In order to parse the FFS directory tree (whether the objective is to dump the
whole thing recursively or to find a specific file given a pathname), one needs
to first (well, after finding the active AB block) find the root directory node.
The root directory object is similar to other directory objects: it has a type
of F2, and an associated chunk of 16 bytes in one of the data sectors.  The
latter contains the name of the root node: on the Pirelli it is "/", whereas on
my GTA02 it is "/ffs-root".

The astute reader should notice that it really makes no sense to store a name
for the root node, and indeed, this name plays no part in the traversal of the
directory tree given an absolute pathname.  But instead this name, or rather
its first character, appears to be used for the purpose of locating the root
node itself.  At first I had assumed that the index record for the root node is
always the first record in the active index block right after the signature
header - that is how it is in "virgin" FFS images, and also in some quite non-
virgin ones I have pulled from my daily-use Pirelli.  Naturally my first version
of the Mokopir-FFS (then called MysteryFFS) extraction utility expected the root
node to always be at index #1.  But then I got some additional Pirelli phones,
and discovered that in certain cases, index record #1 is a deleted object (the
original root node which has been deleted), and the new active root node is
somewhere in the middle of the index!

Thus it appears that in order to find the active root node, one needs to scan
the active index block linearly from the beginning (disregarding the tree
structure pointers in this initial pass), looking for a non-deleted object of
type F2 (a directory) whose corresponding name chunk sports a name beginning
with the '/' character.  (Anyone who's been raised in UNIX will immediately
know that the path separator character '/' is the only character other than NUL
that's absolutely forbidden in the individual filenames - so this special
"root node name" is the only case of a '/' character appearing in what would
otherwise be a regular filename.)

[What causes the root node to be somewhere other than at index #1?  I assume it
 has to do with the dirty space reclamation / data movement algorithm.  In a
 "virgin" FFS image the very first sector is the active index block, and the
 following sector is the first to hold chunks, beginning with the name chunk of
 the root node.  Now what happens if all data in that sector aside from the
 root node name and some other mostly-static directory names becomes dirty,
 i.e., belonging to deleted or overwritten files?  How would that flash space
 get reclaimed?  I assume that the FFS firmware algorithm moves all still-active
 chunks to a new flash sector, invalidating the old copies - turning the latter
 into deleted objects.  The root node will be among them.  Then at some point
 the active index block is going to fill up too, and will need to be rewritten
 into a new sector - at which point the previously-deleted index entries are
 omitted and the root node becomes #1 again...]

Tree structure

Once the root node has been found, the descendant and sibling pointers are used
to traverse the tree structure.  For each directory object, including the root
node, the descendant pointer points to the first child object of this directory:
the first file or subdirectory contained therein.  (Descendant and sibling
pointers take the form of index numbers in the active index block.  A "nil"
pointer is indicated by all 1s (FFFF) - the usual all-0s NULL pointer convention
couldn't be used because it's flash, where the blank state is all 1s.)  If the
descendant pointer of a directory object is nil, that means an empty directory.
The sibling pointer of each file or directory points to its next sibling, i.e.,
the next member of the same parent directory.  The sibling pointer of the root
node is nil.

Data content of files

Objects of type F1 are the head chunks of files.  Each file has a head chunk,
and may or may not have continuation chunks.  More precisely, the head chunk
may contain only the name (or viewed alternatively, 0 bytes of data), or it may
contain a nonzero number of payload bytes; orthogonally to this variability,
there may or may not be continuation chunk(s) present.

Continuation chunks

The descendant pointer of each file head object (the object of type F1, the one
reached by traversing the directory tree) indicates whether or not there are
any continuation chunks present.  If this descendant pointer is nil, there are
no continuation chunks; otherwise it points to the first continuation chunk
object.  File continuation objects have type F4, don't have any siblings (the
sibling pointer is nil - but see below regarding relocated chunks), and the
descendant pointer of each continuation object points to the next continuation
object, if there is one - nil otherwise.

Payload data delineation

Each chunk, whether head or continuation, always has a length that is a nonzero
multiple of 16 bytes.  The length of the chunk here means the amount of flash
space it occupies in its data sector - which is NOT equal to the payload data
length.

The head chunk of each file begins with the filename, terminated by a NUL byte.
If there are any payload data bytes present in this head chunk (I'll explain
momentarily how you would tell), the byte immediately after the NUL that
terminates the filename is the first byte of the payload.  In the case of a
continuation chunk, there is no filename and the first byte of the chunk is the
first byte of that chunk's portion of the user data payload.

Each data-containing chunk (head or continuation) has the following termination
after the last byte of that chunk's payload data: one byte of 00, followed by
however many bytes are needed ([0,15] range) of FFs to pad to a 16-byte
boundary.  A file head chunk that has no payload data has the same format as a
directory name chunk: filename followed by its terminating NUL followed by
[0,15] bytes of FFs to pad to the next 16-byte boundary.

When working with a head chunk, find the beginning of possible payload data (1
byte after the filename terminating NUL) and find the end per the standard
termination logic: scanning from the end of the chunk, skip FFs until 00 is
found (encountering anything else is an error).  If the head chunk has no data,
the effective data length (end_pointer - start_pointer) will be 0 or -1.  (The
latter possibility is the most likely, as there will normally be a "shared" 00
byte, serving as both the filename terminator and the 00 before the padding
FF bytes.)

Relocated chunks

Let's go back to the scenario in which a particular data sector is full (no more
usable free space left) and contains a mixture of active and dirty (deleted or
invalidated) data.  How does the dirty flash space get reclaimed, so that the
amount of available space (blank flash ready to hold new data) becomes equal to
the total FFS size minus the total size of active files and overhead?  It can
only be done by relocating the still-active objects from the full sector to a
new one, invalidating the old copies, and once the old sector consists of
nothing but invalidated data, subjecting it to flash erasure.

So how do the active FFS objects get relocated from a "condemned" sector to a
new one?  If the object is a directory, a new index entry is created, pointing
to the newly relocated name chunk, but it is then made to fit into the old tree
structure without disrupting the latter: the new index entry is added at the
tail of the sibling-chain of the parent directory's descendants, the old index
entry for the same directory is invalidated (as if the directory were rmdir'ed),
and the descendant pointer of the newly written index entry is set to a copy of
the descendant pointer from the old index entry for the same directory.  The
same approach is used when the head chunk of a file needs to be relocated; in
both cases a read-only FFS implementation doesn't need to do anything special to
support reading file and directory objects that have been relocated in this
manner.

However, if the relocated object is a file continuation chunk, then the manner
in which such objects get relocated does affect file reading code.  What if a
chunk in the middle of a chain linked by "descend" pointers needs to be moved?
What happens in this case is that the old copy of the chunk gets invalidated
(the object type byte turned to 00) like in the other object relocating cases,
and the sibling pointer of that old index entry (which was originally FFFF as
continuation objects have no siblings) is set to point to the new index entry
for the same chunk.  The "descend" pointer in the new index entry is a copy of
that pointer from the old index entry.

The manner of chunk relocation just described has been observed in the FFS
images read out of my most recent batch of Pirelli phones - the same ones in
which the root directory object is not at index #1.  Thinking about it as I
write this, I've realized that the way in which continuation objects get
relocated is exactly the same as for other object types - thus the compaction
code in the firmware doesn't need to examine what object type it is moving.
However, the case of continuation chunk relocation deserves special attention
because it affects a read-only implementation like ours - the utilities whose
source accompanies this document used to fail on these FFS images until I
implemented the following additional handling:

When following the chunk chain of a file, normally the only object type that's
expected is F4 - any other object type is an error.  However, as a result of
chunk relocation, one can also encounter deleted objects, i.e., type == 00.
If such a deleted object is encountered, follow its sibling pointer, which must
be non-nil.

Journal file

Every Mokopir-FFS image I've seen so far contains a special file named
/.journal; this file is special in the following ways:

* The object type byte is E1 instead of F1;
* Unlike regular files, this special file is internally-writable.

What I mean by the above is that regular files are mostly immutable: once a
file has been created with some data content in the head chunk, it can only be
either appended to (one or more continuation chunks added), or overwritten by
creating a new file with the same name at the same level in the tree hierarchy
and invalidating the old one.  But the special /.journal file is different: I
have never observed it to consist of more than the head chunk, and this head
chunk is pre-allocated with some largish and apparently fixed length (4 KiB on
my GTA02, 16 KiB on the Pirelli).  This pre-allocated chunk contains what look
like 16-byte records at the beginning (on the first 4-byte boundary after the
NUL terminating the ".journal" name), followed by blank flash for the remainder
of the pre-allocated chunk - so it surely looks like new flash writes happen
within this chunk.

I do not currently know the purpose of this /.journal file or the meaning of the
records it seems to contain.  This understanding would surely be needed if one
wanted to create FFS images from scratch or to implement FFS write operations,
but I reason that a read-only implementation can get away with simply ignoring
this file.  I reason that this file can't be necessary in order to parse an FFS
image for reading because one needs to parse the tree structure first in order
to locate this journal file itself.

-------------------------------------------------------------------------------

That's all I can think of right now.  If anything is unclear, see the
accompanying source code for the listing/extraction utilities: with the general
explanation given by this document, it should be clear what my code does and
why.  And if a given piece of knowledge is found neither in this document nor
in my source code, then I don't know it myself either, and my read-only
Mokopir-FFS implementation makes do without it.

All knowledge contained herein has been recovered by reverse engineering.
Believe it or not, I have figured it out by staring at the hex dump of FFS
sectors, reasoning about how one could possibly implement an FFS given the
requirement of dynamic writability and the physical constraints of flash memory,
and writing listing/extraction test code iteratively until I got something that
appears to correctly parse all FFS images available to me - the result is the
code in this package.

I never got as far as attempting to locate the FFS implementation routines
within the proprietary firmware binary code images, and I haven't found an
implementation of this particular FFS in any of the leaked sources yet either.
The TSM30 code doesn't seem to be of any use as its FFS appears to be totally
different.  As to the more recently found LoCosto code leak, I found that one a
few days *after* I got the Moko/Pirelli "MysteryFFS" reverse-engineered on my
own, and when I did look at the FFS in the LoCosto code later, I saw what seems
to be a different FFS as well.

Michael Spacefalcon
SE 52 Mes 16