|
205 | 205 | return std::vector<uint8_t>(bytes.data() + sig_off, bytes.data() + sig_off + sig_size); |
206 | 206 | } |
207 | 207 |
|
| 208 | +// Simulates a file truncated between fstat() and a later pread(): Size() |
| 209 | +// reports a larger value than the data actually contains, so a read past |
| 210 | +// data_.size() returns 0 (EOF) while the verifier still believes there |
| 211 | +// are more bytes to consume. |
| 212 | +class TruncatedMemoryFileReader : public santa::FileReader { |
| 213 | + public: |
| 214 | + TruncatedMemoryFileReader(std::vector<uint8_t> data, off_t claimed_size) |
| 215 | + : data_(std::move(data)), claimed_size_(claimed_size) {} |
| 216 | + ssize_t Pread(void* buf, size_t len, off_t off) override { |
| 217 | + if (off < 0) { |
| 218 | + errno = EINVAL; |
| 219 | + return -1; |
| 220 | + } |
| 221 | + if (static_cast<size_t>(off) >= data_.size()) return 0; |
| 222 | + size_t available = data_.size() - static_cast<size_t>(off); |
| 223 | + size_t n = std::min(len, available); |
| 224 | + std::memcpy(buf, data_.data() + off, n); |
| 225 | + return static_cast<ssize_t>(n); |
| 226 | + } |
| 227 | + off_t Size() const override { return claimed_size_; } |
| 228 | + |
| 229 | + private: |
| 230 | + std::vector<uint8_t> data_; |
| 231 | + off_t claimed_size_; |
| 232 | +}; |
| 233 | + |
208 | 234 | } // namespace |
209 | 235 |
|
210 | 236 | @interface VerifyingHasherCoreTest : XCTestCase |
@@ -282,6 +308,26 @@ - (void)testIoErrorPropagated { |
282 | 308 | XCTAssertTrue(v.FullFileDigest().empty()); |
283 | 309 | } |
284 | 310 |
|
| 311 | +// Phase 5 (tail) used to silently `break` when pread returned 0 with |
| 312 | +// cursor_ < total, finalizing the digest over only a prefix of the |
| 313 | +// file. The fix classifies that as kIoError, matching phases 1 and 3. |
| 314 | +// We simulate the trigger with a reader that claims a larger Size() |
| 315 | +// than its actual data — the verifier completes header / cs-blob / |
| 316 | +// signed-region phases normally, then the tail drain hits EOF early. |
| 317 | +- (void)testTailEofClassifiedAsIoError { |
| 318 | + auto bytes = Slurp("/usr/bin/yes"); |
| 319 | + XCTAssertFalse(bytes.empty()); |
| 320 | + const off_t real_size = static_cast<off_t>(bytes.size()); |
| 321 | + TruncatedMemoryFileReader r(std::move(bytes), real_size + 1024 * 1024); |
| 322 | + VerifyingHasherCore v(r, kHostArch); |
| 323 | + auto s = v.Run(); |
| 324 | + XCTAssertEqual(s, VerifyingHasherCore::Status::kIoError); |
| 325 | + XCTAssertTrue(v.FullFileDigest().empty()); |
| 326 | + XCTAssertNotEqual(std::string(v.LastError()).find("tail"), std::string::npos, |
| 327 | + @"LastError should point at the tail phase: %s", |
| 328 | + std::string(v.LastError()).c_str()); |
| 329 | +} |
| 330 | + |
285 | 331 | - (void)testSinglePassInvariant { |
286 | 332 | auto bytes = Slurp("/usr/bin/yes"); |
287 | 333 | XCTAssertFalse(bytes.empty()); |
|
0 commit comments