Skip to content

Commit 00eb2bf

Browse files
chronos: disable network in replay builds and extend docs (google#14136)
The documentation needs a larger revamp, will do in a follow-up. --------- Signed-off-by: David Korczynski <[email protected]>
1 parent 09f700a commit 00eb2bf

File tree

3 files changed

+143
-51
lines changed

3 files changed

+143
-51
lines changed

infra/experimental/chronos/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,70 @@
11
# Chronos: rebuilding OSS-Fuzz harnesses using cached builds
22

3+
Chronos is a utility tooling to enable fast re-building of OSS-Fuzz projects
4+
and analysis of projects' testing infrastructure. This is used by projects,
5+
e.g. [OSS-Fuzz-gen](https://github.com/google/oss-fuzz-gen) to help speed up
6+
valuation processes during fuzzing harness generation.
7+
8+
Chronos is focused on two features, rebuilding projects fast and running the tests of a given project.
9+
10+
## Rebuilding projects fast
11+
12+
Chronos enables rebuilding projects efficiently in contexts where only a small patch
13+
needs to be evalualted in the target. This is achieved by running a replay build script
14+
in the build container, similarly to how a regular `build_fuzzers` command would run, but
15+
with the caveat that the replay build script only performs a subset of the operations
16+
of the original `build.sh`.
17+
18+
The replay build scripts are constructed in two ways: manually or automatically.
19+
20+
### Automated rebuilds
21+
22+
Chronos support automated rebuilding by:
23+
24+
1. Calling into a `replay_build.sh` script during the building inside the container [here](https://github.com/google/oss-fuzz/blob/206656447b213fb04901d15122692d8dd4d45312/infra/base-images/base-builder/compile#L292-L296)
25+
2. The `replay_build.sh` calls into `make_build_replayable.py`: [here](https://github.com/google/oss-fuzz/blob/master/infra/base-images/base-builder/replay_build.sh)
26+
3. `make_build_replayable.py` adjusts the build environment to wrap around common commands, to avoid performing a complete run of `build.sh`: [here](https://github.com/google/oss-fuzz/blob/master/infra/base-images/base-builder/make_build_replayable.py).
27+
28+
### Manually provided replay builds
29+
30+
`replay_build.sh` above, is simply just a wrapper script around `build.sh` that aims to enable
31+
fast rebuilding of the project. This `replay_build.sh` can, however, be overwritten in the Dockerfile
32+
of the project's builder image. Examples of this is [php](https://github.com/google/oss-fuzz/blob/206656447b213fb04901d15122692d8dd4d45312/projects/php/replay_build.sh#L1) and [ffmpeg](https://github.com/google/oss-fuzz/blob/master/projects/ffmpeg/replay_build.sh#L1).
33+
34+
Providing a manual `replay_build.sh` is likely more efficient at build time and can help speed up the process. Automated replay build scripts can also be erroneous.
35+
36+
37+
### Testing the validity of a replay build
38+
39+
The Chronos manager can use the `manager.py` to validate the validity of a
40+
replay build for a given project:
41+
42+
```sh
43+
python3 infra/experimental/chronos/manager.py check-test tinyobjloader
44+
```
45+
46+
If the above command fails for the relevant project, then the replay build feature
47+
does not work for the given project.
48+
49+
## Running tests of a project
50+
51+
The second part of Chronos is a feature to enable running the tests of a given
52+
project. This is done by way of a script `run_tests.sh`. Samples of
53+
this script include [jsonnet](https://github.com/google/oss-fuzz/blob/master/projects/jsonnet/run_tests.sh#L1) and [tinyobjloader](https://github.com/google/oss-fuzz/blob/master/projects/tinyobjloader/run_tests.sh#L1).
54+
55+
56+
### Testing the validity of run_tests.sh
57+
58+
The Chronos manager can use the `manager.py` to validate the validity of a
59+
`run_tests.sh` script:
60+
61+
```sh
62+
python3 infra/experimental/chronos/manager.py
63+
```
64+
65+
66+
**Running tests of a project**
67+
368
## Pre-built images.
469

570
Daily pre-built images are available at:

infra/experimental/chronos/bad_patch.py

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import sys
2020

2121
import tree_sitter_cpp
22-
from tree_sitter import Language, Node, Parser, Query, QueryCursor
22+
from tree_sitter import Language, Parser, Query, QueryCursor
2323

2424
LANGUAGE = Language(tree_sitter_cpp.language())
2525
PARSER = Parser(LANGUAGE)
@@ -30,7 +30,27 @@
3030

3131
def normal_compile():
3232
"""Do nothing and act as a control test that should always success."""
33-
pass
33+
34+
35+
def source_code_white_noise():
36+
"""Insert white noise. This is a control test which forces
37+
recompilation of good code. We need this to make sure that
38+
the system is able to rebuild full source code under the
39+
Chronos environment."""
40+
exts = ['.c', '.cc', '.cpp', '.cxx', '.h', '.hpp']
41+
payload = '\n\n\n\n\n\n'
42+
43+
# Walk and insert garbage code
44+
for cur, dirs, files in os.walk(ROOT_PATH):
45+
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
46+
for file in files:
47+
if any(file.endswith(ext) for ext in exts):
48+
path = os.path.join(cur, file)
49+
try:
50+
with open(path, 'a', encoding='utf-8') as f:
51+
f.write(payload)
52+
except Exception:
53+
pass
3454

3555

3656
def source_code_compile_error():
@@ -45,7 +65,7 @@ def source_code_compile_error():
4565
if any(file.endswith(ext) for ext in exts):
4666
path = os.path.join(cur, file)
4767
try:
48-
with open(path, 'a') as f:
68+
with open(path, 'a', encoding='utf-8') as f:
4969
f.write(payload)
5070
except Exception:
5171
pass
@@ -63,7 +83,7 @@ def macro_compile_error():
6383
if any(file.endswith(ext) for ext in exts):
6484
path = os.path.join(cur, file)
6585
try:
66-
with open(path, 'a') as f:
86+
with open(path, 'a', encoding='utf-8') as f:
6787
f.write(payload)
6888
except Exception:
6989
pass
@@ -92,13 +112,13 @@ def missing_header_error():
92112
try:
93113
# Read source file
94114
source = ''
95-
with open(path, 'r') as f:
115+
with open(path, 'r', encoding='utf-8') as f:
96116
source = f.read()
97117
if not source:
98118
continue
99119

100120
# Append a wrong header inclusion at the beginning
101-
with open(path, 'w') as f:
121+
with open(path, 'w', encoding='utf-8') as f:
102122
f.write(payload)
103123
f.write(source)
104124
count += 1
@@ -126,11 +146,11 @@ def duplicate_symbol_error():
126146
try:
127147
# Try read and parse the source with tree-sitter
128148
source = ''
129-
with open(path, 'r') as f:
149+
with open(path, 'r', encoding='utf-8') as f:
130150
source = f.read()
131151
if source:
132152
node = PARSER.parse(source.encode()).root_node
133-
except:
153+
except Exception:
134154
pass
135155

136156
if not node:
@@ -147,10 +167,10 @@ def duplicate_symbol_error():
147167
# Add source code with duplicated declaration randomly
148168
if new_source and random.choice([True, False]):
149169
try:
150-
with open(path, 'w') as f:
170+
with open(path, 'w', encoding='utf-8') as f:
151171
f.write(new_source)
152172
count += 1
153-
except:
173+
except Exception:
154174
pass
155175

156176

@@ -174,18 +194,18 @@ def function_linker_error():
174194
try:
175195
# Read source file
176196
source = ''
177-
with open(path, 'r') as f:
197+
with open(path, 'r', encoding='utf-8') as f:
178198
source = f.read()
179199
if not source:
180200
continue
181201

182202
# Append a wrong header inclusion at the beginning
183-
with open(path, 'w') as f:
203+
with open(path, 'w', encoding='utf-8') as f:
184204
f.write(payload)
185205
f.write(source)
186206
f.write(payload_call.replace('{COUNT}', str(count)))
187207
count += 1
188-
except:
208+
except Exception:
189209
pass
190210

191211

@@ -194,6 +214,10 @@ def function_linker_error():
194214
'func': normal_compile,
195215
'rc': [0],
196216
},
217+
'white_noise': {
218+
'func': source_code_white_noise,
219+
'rc': [0],
220+
},
197221
'compile_error': {
198222
'func': source_code_compile_error,
199223
'rc': [1, 2],
@@ -218,6 +242,7 @@ def function_linker_error():
218242

219243

220244
def main():
245+
"""Main entrypoint."""
221246
target = sys.argv[1]
222247
BAD_PATCH_GENERATOR[target]['func']()
223248

infra/experimental/chronos/manager.py

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def build_project_image(project):
8585
def build_cached_project(project, cleanup=True, sanitizer='address'):
8686
"""Build cached image for a project."""
8787
container_name = _get_project_cached_named_local(project, sanitizer)
88-
88+
logger.info('Building cached image for project: %s', project)
8989
# Clean up the container if it exists.
9090
if cleanup:
9191
try:
@@ -104,11 +104,16 @@ def build_cached_project(project, cleanup=True, sanitizer='address'):
104104
f'--env=FUZZING_LANGUAGE={project_language}',
105105
'--env=CAPTURE_REPLAY_SCRIPT=1', f'--name={container_name}',
106106
f'-v={cwd}/ccaches/{project}/ccache:/workspace/ccache',
107-
f'-v={cwd}/build/out/{project}/:/out/', f'gcr.io/oss-fuzz/{project}',
108-
'bash', '-c',
109-
'"export PATH=/ccache/bin:\$PATH && compile && cp -n /usr/local/bin/replay_build.sh \$SRC/"'
107+
f'-v={cwd}/build/out/{project}/:/out/',
108+
'-v=' + os.path.join(os.getcwd(), 'infra', 'experimental', 'chronos') +
109+
':/chronos/', f'gcr.io/oss-fuzz/{project}', 'bash', '-c',
110+
('"export PATH=/ccache/bin:\$PATH && python3.11 -m pip install -r /chronos/requirements.txt && '
111+
'rm -rf /out/* && compile && cp -n /usr/local/bin/replay_build.sh \$SRC/"'
112+
)
110113
]
111114

115+
logger.info('Command: %s', ' '.join(cmd))
116+
112117
start = time.time()
113118
try:
114119
subprocess.check_call(' '.join(cmd), shell=True)
@@ -162,14 +167,18 @@ def build_cached_project(project, cleanup=True, sanitizer='address'):
162167
def check_cached_replay(project, sanitizer='address', integrity_test=False):
163168
"""Checks if a cache build succeeds and times is."""
164169
build_project_image(project)
165-
build_cached_project(project, sanitizer=sanitizer)
170+
if not build_cached_project(project, sanitizer=sanitizer):
171+
logger.info('Failed to build cached image for project: %s', project)
172+
return
166173

167174
start = time.time()
168-
base_cmd = 'export PATH=/ccache/bin:$PATH && rm -rf /out/* && compile'
175+
base_cmd = 'export PATH=/ccache/bin:\\$PATH && rm -rf /out/* && compile'
169176
cmd = [
170177
'docker',
171178
'run',
172179
'--rm',
180+
'--network',
181+
'none',
173182
'--env=SANITIZER=' + sanitizer,
174183
'--env=FUZZING_LANGUAGE=c++',
175184
'-v=' + os.path.join(os.getcwd(), 'build', 'out', project) + ':/out',
@@ -187,9 +196,7 @@ def check_cached_replay(project, sanitizer='address', integrity_test=False):
187196
for bad_patch_name, bad_patch_map in bad_patch.BAD_PATCH_GENERATOR.items():
188197
# Generate bad patch command using different approaches
189198
expected_rc = bad_patch_map['rc']
190-
bad_patch_command = (
191-
'python3 -m pip install -r /chronos/requirements.txt && '
192-
f'python3 /chronos/bad_patch.py {bad_patch_name}')
199+
bad_patch_command = (f'python3 /chronos/bad_patch.py {bad_patch_name}')
193200
cmd_to_run = cmd[:]
194201
cmd_to_run.append(
195202
f'"set -euo pipefail && {bad_patch_command} && {base_cmd}"')
@@ -203,17 +210,25 @@ def check_cached_replay(project, sanitizer='address', integrity_test=False):
203210
'Return code: %d. Expected return code: %s'), project,
204211
bad_patch_name, result.returncode, str(expected_rc))
205212

206-
if failed:
207-
logger.info(
208-
'%s check cached replay failed to detect these bad patches: %s',
209-
project, ' '.join(failed))
210-
else:
211-
logger.info('%s check cached replay success to detect all bad patches.',
212-
project)
213+
if failed:
214+
logger.info(
215+
'%s check cached replay failed to detect these bad patches: %s',
216+
project, ' '.join(failed))
217+
else:
218+
logger.info('%s check cached replay success to detect all bad patches.',
219+
project)
213220
else:
214221
# Normal run with no integrity check
215222
cmd.append(f'"{base_cmd}"')
216-
subprocess.run(' '.join(cmd), shell=True, check=False)
223+
replay_success = False
224+
try:
225+
subprocess.run(' '.join(cmd), shell=True, check=True)
226+
replay_success = True
227+
except subprocess.CalledProcessError as e:
228+
logger.error('Failed to run cached replay: %s', e)
229+
replay_success = False
230+
logger.info('%s check cached replay: %s.', project,
231+
'succeeded' if replay_success else 'failed')
217232

218233
end = time.time()
219234
logger.info('%s check cached replay completion time: %.2f seconds', project,
@@ -266,7 +281,7 @@ def check_test(project,
266281
for logic_patch in logic_error_patch.LOGIC_ERROR_PATCHES:
267282
logger.info('Checking logic patch: %s', logic_patch.name)
268283
patch_command = (
269-
'python3 -m pip install -r /chronos/requirements.txt && '
284+
'python3 -m pip install -r /chronos/requirements.txt &&'
270285
f'python3 /chronos/logic_error_patch.py {logic_patch.name} && '
271286
'compile')
272287
cmd_to_run = docker_cmd[:]
@@ -537,11 +552,9 @@ def _cmd_dispatcher_check_test(args):
537552

538553

539554
def _cmd_dispatcher_check_replay(args):
540-
check_cached_replay(args.project, args.sanitizer)
541-
542-
543-
def _cmd_dispatcher_check_replay_integrity(args):
544-
check_cached_replay(args.project, args.sanitizer, integrity_test=True)
555+
check_cached_replay(args.project,
556+
args.sanitizer,
557+
integrity_test=args.integrity_test)
545558

546559

547560
def _cmd_dispatcher_build_cached_image(args):
@@ -616,20 +629,10 @@ def parse_args():
616629
'--sanitizer',
617630
default='address',
618631
help='The sanitizer to use for the cached build (default: address).')
619-
620-
check_replay_script_integrity_parser = subparsers.add_parser(
621-
'check-replay-script-integrity',
622-
help=
623-
('Checks if the replay script works for a specific project. '
624-
'Integrity of the replay script is also tested with different bad patches.'
625-
))
626-
627-
check_replay_script_integrity_parser.add_argument(
628-
'project', help='The name of the project to check.')
629-
check_replay_script_integrity_parser.add_argument(
630-
'--sanitizer',
631-
default='address',
632-
help='The sanitizer to use for the cached build (default: address).')
632+
check_replay_script_parser.add_argument(
633+
'--integrity-test',
634+
action='store_true',
635+
help='If set, will test the integrity of the replay script.')
633636

634637
check_run_tests_script_parser = subparsers.add_parser(
635638
'check-run-tests-script',
@@ -723,7 +726,6 @@ def main():
723726
dispatch_map = {
724727
'check-test': _cmd_dispatcher_check_test,
725728
'check-replay-script': _cmd_dispatcher_check_replay,
726-
'check-replay-script-integrity': _cmd_dispatcher_check_replay_integrity,
727729
'build-cached-image': _cmd_dispatcher_build_cached_image,
728730
'autogen-tests': _cmd_dispatcher_autogen_tests,
729731
'build-many-caches': _cmd_dispatcher_build_many_caches,

0 commit comments

Comments
 (0)