1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "XZStream.h"
8 
9 #include <algorithm>
10 #include <cstring>
11 #include "mozilla/Assertions.h"
12 #include "mozilla/CheckedInt.h"
13 #include "Logging.h"
14 
15 // LZMA dictionary size, should have a minimum size for the given compression
16 // rate, see XZ Utils docs for details.
17 static const uint32_t kDictSize = 1 << 24;
18 
19 static const size_t kFooterSize = 12;
20 
21 // Parses a variable-length integer (VLI),
22 // see http://tukaani.org/xz/xz-file-format.txt for details.
ParseVarLenInt(const uint8_t * aBuf,size_t aBufSize,uint64_t * aValue)23 static size_t ParseVarLenInt(const uint8_t* aBuf, size_t aBufSize,
24                              uint64_t* aValue) {
25   if (!aBufSize) {
26     return 0;
27   }
28   aBufSize = std::min(size_t(9), aBufSize);
29 
30   *aValue = aBuf[0] & 0x7F;
31   size_t i = 0;
32 
33   while (aBuf[i++] & 0x80) {
34     if (i >= aBufSize || aBuf[i] == 0x0) {
35       return 0;
36     }
37     *aValue |= static_cast<uint64_t>(aBuf[i] & 0x7F) << (i * 7);
38   }
39   return i;
40 }
41 
42 /* static */
IsXZ(const void * aBuf,size_t aBufSize)43 bool XZStream::IsXZ(const void* aBuf, size_t aBufSize) {
44   static const uint8_t kXzMagic[] = {0xfd, '7', 'z', 'X', 'Z', 0x0};
45   MOZ_ASSERT(aBuf);
46   return aBufSize > sizeof(kXzMagic) &&
47          !memcmp(reinterpret_cast<const void*>(kXzMagic), aBuf,
48                  sizeof(kXzMagic));
49 }
50 
XZStream(const void * aInBuf,size_t aInSize)51 XZStream::XZStream(const void* aInBuf, size_t aInSize)
52     : mInBuf(static_cast<const uint8_t*>(aInBuf)),
53       mUncompSize(0),
54       mDec(nullptr) {
55   mBuffers.in = mInBuf;
56   mBuffers.in_pos = 0;
57   mBuffers.in_size = aInSize;
58 }
59 
~XZStream()60 XZStream::~XZStream() { xz_dec_end(mDec); }
61 
Init()62 bool XZStream::Init() {
63 #ifdef XZ_USE_CRC64
64   xz_crc64_init();
65 #endif
66   xz_crc32_init();
67 
68   mDec = xz_dec_init(XZ_DYNALLOC, kDictSize);
69 
70   if (!mDec) {
71     return false;
72   }
73 
74   mUncompSize = ParseUncompressedSize();
75   if (!mUncompSize) {
76     return false;
77   }
78 
79   return true;
80 }
81 
Decode(void * aOutBuf,size_t aOutSize)82 size_t XZStream::Decode(void* aOutBuf, size_t aOutSize) {
83   if (!mDec) {
84     return 0;
85   }
86 
87   mBuffers.out = static_cast<uint8_t*>(aOutBuf);
88   mBuffers.out_pos = 0;
89   mBuffers.out_size = aOutSize;
90 
91   while (mBuffers.in_pos < mBuffers.in_size &&
92          mBuffers.out_pos < mBuffers.out_size) {
93     const xz_ret ret = xz_dec_run(mDec, &mBuffers);
94 
95     switch (ret) {
96       case XZ_STREAM_END:
97         // Stream ended, the next loop iteration should terminate.
98         MOZ_ASSERT(mBuffers.in_pos == mBuffers.in_size);
99         [[fallthrough]];
100 #ifdef XZ_DEC_ANY_CHECK
101       case XZ_UNSUPPORTED_CHECK:
102         // Ignore unsupported check.
103         [[fallthrough]];
104 #endif
105       case XZ_OK:
106         // Chunk decoded, proceed.
107         break;
108 
109       case XZ_MEM_ERROR:
110         ERROR("XZ decoding: memory allocation failed");
111         return 0;
112 
113       case XZ_MEMLIMIT_ERROR:
114         ERROR("XZ decoding: memory usage limit reached");
115         return 0;
116 
117       case XZ_FORMAT_ERROR:
118         ERROR("XZ decoding: invalid stream format");
119         return 0;
120 
121       case XZ_OPTIONS_ERROR:
122         ERROR("XZ decoding: unsupported header options");
123         return 0;
124 
125       case XZ_DATA_ERROR:
126         [[fallthrough]];
127       case XZ_BUF_ERROR:
128         ERROR("XZ decoding: corrupt input stream");
129         return 0;
130 
131       default:
132         MOZ_ASSERT_UNREACHABLE("XZ decoding: unknown error condition");
133         return 0;
134     }
135   }
136   return mBuffers.out_pos;
137 }
138 
RemainingInput() const139 size_t XZStream::RemainingInput() const {
140   return mBuffers.in_size - mBuffers.in_pos;
141 }
142 
Size() const143 size_t XZStream::Size() const { return mBuffers.in_size; }
144 
UncompressedSize() const145 size_t XZStream::UncompressedSize() const { return mUncompSize; }
146 
ParseIndexSize() const147 size_t XZStream::ParseIndexSize() const {
148   static const uint8_t kFooterMagic[] = {'Y', 'Z'};
149 
150   const uint8_t* footer = mInBuf + mBuffers.in_size - kFooterSize;
151   // The magic bytes are at the end of the footer.
152   if (memcmp(reinterpret_cast<const void*>(kFooterMagic),
153              footer + kFooterSize - sizeof(kFooterMagic),
154              sizeof(kFooterMagic))) {
155     // Not a valid footer at stream end.
156     ERROR("XZ parsing: Invalid footer at end of stream");
157     return 0;
158   }
159   // Backward size is a 32 bit LE integer field positioned after the 32 bit
160   // CRC32 code. It encodes the index size as a multiple of 4 bytes with a
161   // minimum size of 4 bytes.
162   const uint32_t backwardSizeRaw = *(footer + 4);
163   // Check for overflow.
164   mozilla::CheckedInt<size_t> backwardSizeBytes(backwardSizeRaw);
165   backwardSizeBytes = (backwardSizeBytes + 1) * 4;
166   if (!backwardSizeBytes.isValid()) {
167     ERROR("XZ parsing: Cannot parse index size");
168     return 0;
169   }
170   return backwardSizeBytes.value();
171 }
172 
ParseUncompressedSize() const173 size_t XZStream::ParseUncompressedSize() const {
174   static const uint8_t kIndexIndicator[] = {0x0};
175 
176   const size_t indexSize = ParseIndexSize();
177   if (!indexSize) {
178     return 0;
179   }
180   // The footer follows directly the index, so we can use it as a reference.
181   const uint8_t* end = mInBuf + mBuffers.in_size;
182   const uint8_t* index = end - kFooterSize - indexSize;
183 
184   // The xz stream index consists of three concatenated elements:
185   //  (1) 1 byte indicator (always OxOO)
186   //  (2) a Variable Length Integer (VLI) field for the number of records
187   //  (3) a list of records
188   // See https://tukaani.org/xz/xz-file-format-1.0.4.txt
189   // Each record contains a VLI field for unpadded size followed by a var field
190   // for uncompressed size. We only support xz streams with a single record.
191 
192   if (memcmp(reinterpret_cast<const void*>(kIndexIndicator), index,
193              sizeof(kIndexIndicator))) {
194     ERROR("XZ parsing: Invalid stream index");
195     return 0;
196   }
197 
198   index += sizeof(kIndexIndicator);
199   uint64_t numRecords = 0;
200   index += ParseVarLenInt(index, end - index, &numRecords);
201   // Only streams with a single record are supported.
202   if (numRecords != 1) {
203     ERROR("XZ parsing: Multiple records not supported");
204     return 0;
205   }
206   uint64_t unpaddedSize = 0;
207   index += ParseVarLenInt(index, end - index, &unpaddedSize);
208   if (!unpaddedSize) {
209     ERROR("XZ parsing: Unpadded size is 0");
210     return 0;
211   }
212   uint64_t uncompressedSize = 0;
213   index += ParseVarLenInt(index, end - index, &uncompressedSize);
214   mozilla::CheckedInt<size_t> checkedSize(uncompressedSize);
215   if (!checkedSize.isValid()) {
216     ERROR("XZ parsing: Uncompressed stream size is too large");
217     return 0;
218   }
219 
220   return checkedSize.value();
221 }
222