mirror of
https://github.com/facebook/sapling.git
synced 2024-10-10 16:57:49 +03:00
7400585a0b
Summary: There's a bug in some combination of Eden and FUSE where open(O_TRUNC) followed by a sequence of writes over an existing file does not flush the kernel's VFS page cache, which manifests as an mmap larger than the file's size not zeroing the data beyond the file's size. These tests attempt capture that use case, but they are fiddly. Disabling ATOMIC_O_TRUNC seems to resolve the issue. Reviewed By: wez Differential Revision: D6430152 fbshipit-source-id: f7626e268e778ebab60c66322e0ce42bce746ae1
108 lines
3.9 KiB
Python
108 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
#
|
|
# Copyright (c) 2016-present, Facebook, Inc.
|
|
# All rights reserved.
|
|
#
|
|
# This source code is licensed under the BSD-style license found in the
|
|
# LICENSE file in the root directory of this source tree. An additional grant
|
|
# of patent rights can be found in the PATENTS file in the same directory.
|
|
|
|
from .lib import testcase
|
|
import ctypes
|
|
from ctypes import c_void_p, c_size_t, c_ssize_t, c_int
|
|
import os
|
|
import mmap
|
|
import subprocess
|
|
|
|
|
|
# python's mmap does not allow mapping larger than the file's size
|
|
libc = ctypes.CDLL('libc.so.6')
|
|
c_off_t = c_ssize_t
|
|
|
|
libc.mmap.argtypes = [c_void_p, c_size_t, c_int, c_int, c_off_t]
|
|
libc.mmap.restype = ctypes.POINTER(ctypes.c_byte)
|
|
|
|
libc.munmap.restype = c_void_p
|
|
libc.munmap.argtypes = [c_void_p, c_size_t]
|
|
|
|
|
|
@testcase.eden_repo_test
|
|
class MmapTest:
|
|
contents = 'abcdef'
|
|
|
|
def populate_repo(self):
|
|
self.repo.write_file('filename', self.contents)
|
|
self.repo.commit('Initial commit.')
|
|
self.filename = os.path.join(self.mount, 'filename')
|
|
|
|
def test_mmap_in_backed_file_is_null_terminated(self):
|
|
fd = os.open(self.filename, os.O_RDONLY)
|
|
try:
|
|
size = os.fstat(fd).st_size
|
|
self.assertEqual(len(self.contents), size)
|
|
|
|
map_size = (size + 4095) // 4096 * 4096
|
|
self.assertNotEqual(size, map_size)
|
|
|
|
m = libc.mmap(None, map_size, mmap.PROT_READ, mmap.MAP_PRIVATE, fd, 0)
|
|
try:
|
|
# assert the additional mapped bytes are null, per `man 2 mmap`
|
|
for i in range(size, map_size):
|
|
self.assertEqual(0, m[i])
|
|
finally:
|
|
libc.munmap(m, map_size)
|
|
finally:
|
|
os.close(fd)
|
|
|
|
def test_mmap_is_null_terminated_after_truncate_and_write_to_overlay(self):
|
|
# WARNING: This test is very fiddly.
|
|
|
|
# The bug is that if a file in Eden is opened with O_TRUNC followed by
|
|
# a series of writes, then mmap of that file with a size larger than the
|
|
# file (but still within the trailing page) does not zero the trailing
|
|
# bytes. Clang relies on this mmap behavior to enforce that the buffer
|
|
# is null-terminated. Since the buffer ends up not being null-
|
|
# terminated, Clang segfaults.
|
|
#
|
|
# It seems like this is a kernel or FUSE bug more than an Eden bug,
|
|
# but we should verify nonetheless that it does not occur.
|
|
|
|
# If this test uses the same file committed in populate_repo, the bug
|
|
# does not reproduce.
|
|
filename = os.path.join(self.mount, 'filename2')
|
|
|
|
# Write to the file from another process. if this process writes the
|
|
# file, the bug is not reproduced.
|
|
subprocess.check_call(
|
|
['dd', 'if=/dev/urandom', 'of=' + filename, 'bs=4096', 'count=6'])
|
|
|
|
# A few pages, with data slightly beyond a page boundary.
|
|
new_contents = b'abcd' * 3072 + b'abcdef'
|
|
new_size = len(new_contents)
|
|
|
|
# Write to the file with another process. if this process writes the
|
|
# file, the bug is not reproduced.
|
|
with subprocess.Popen(
|
|
['dd', 'of=' + filename, 'bs=512'],
|
|
stdin=subprocess.PIPE) as p:
|
|
p.stdin.write(new_contents)
|
|
|
|
fd = os.open(filename, os.O_RDONLY)
|
|
try:
|
|
size = os.fstat(fd).st_size
|
|
self.assertEqual(new_size, size)
|
|
|
|
# Map all the way up to a page boundary.
|
|
map_size = (size + 4095) // 4096 * 4096
|
|
self.assertNotEqual(size, map_size)
|
|
|
|
m = libc.mmap(None, map_size, mmap.PROT_READ, mmap.MAP_PRIVATE, fd, 0)
|
|
try:
|
|
# Assert the additional mapped bytes are null, per `man 2 mmap`
|
|
for i in range(size, map_size):
|
|
self.assertEqual(0, m[i])
|
|
finally:
|
|
libc.munmap(m, map_size)
|
|
finally:
|
|
os.close(fd)
|