| #!/usr/bin/env python3 | 
 |  | 
 | # Copyright 2025 The Chromium Authors | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 |  | 
 | # This script is used to list files that require `#pragma allow_unsafe_buffers` | 
 | # in the Chromium codebase. | 
 | # | 
 | # Prerequisites: | 
 | # -------------- | 
 | # Ensure your .gclient contains: | 
 | # ``` | 
 | # target_os = ["win", "android", "linux", "chromeos", "mac", "fuchsia"] | 
 | # solutions = [ | 
 | #   { | 
 | #     ... | 
 | #     "custom_vars": { | 
 | #       "checkout_src_internal": True, | 
 | #       "download_remoteexec_cfg": True, | 
 | #       "checkout_pgo_profiles": True, | 
 | #       "checkout_mobile_internal": True, | 
 | #       "checkout_google_internal": True, | 
 | #     }, | 
 | #  }, | 
 | #] | 
 | # ``` | 
 | # You'll also need to run some scripts like: | 
 | # ``` | 
 | # build/linux/sysroot_scripts/install-sysroot.py --arch=arm | 
 | # build/linux/sysroot_scripts/install-sysroot.py --arch=arm64 | 
 | # gclient sync -f -D | 
 | # ``` | 
 | # | 
 | # Usage for automatic spanification | 
 | # --------------------------------- | 
 | # By running this script we can determine remove files that have been fixed | 
 | # (not 100% exhaustive). | 
 | # | 
 | # Example: | 
 | # | 
 | # 1. Checkout "main" | 
 | # 2. Generate a spanification patch: "rewrite" (or run at HEAD) | 
 | # 3. Run this script to remove unneeded pragmas: "pragma-after". | 
 | # 4. Commit "pragma-after". | 
 |  | 
 | import json | 
 | import os | 
 | import subprocess | 
 | import sys | 
 |  | 
 | # common gn args for spanify project scripts. | 
 | from gnconfigs import GnConfigs, GenerateGnTarget | 
 |  | 
 | # Building is going to fail on multiple files. They will be fixed automatically | 
 | # inserting `opt_out_lines` in the file after the copyright notice. | 
 | opt_out_lines = [ | 
 |     "", | 
 |     "#ifdef UNSAFE_BUFFERS_BUILD", | 
 |     "// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.", | 
 |     "#pragma allow_unsafe_buffers", | 
 |     "#endif", | 
 | ] | 
 |  | 
 |  | 
 | # Looks through all potential code files for any mention of | 
 | # 'allow_unsafe_buffers', note that this will be a super set of ones that | 
 | # actually have the pragma because it could just be mentioned in comments. | 
 | def FindCodeFilesWithPragma() -> set[str]: | 
 |     files_with_pragma = """ | 
 |         git grep -l '#pragma allow_unsafe_buffers' --\ | 
 |             '*.h' '*.cc' '*.c' '*.cpp' '*.mm' '*.m' | 
 |     """ | 
 |     files_that_had_pragma = set(f.strip() for f in subprocess.check_output( | 
 |         files_with_pragma, shell=True).decode("utf-8").split("\n")) | 
 |     files_that_had_pragma.discard("") | 
 |     return files_that_had_pragma | 
 |  | 
 |  | 
 | # For a given target (just a label for the out directory) and the associated | 
 | # gn args generate the json representation of the targets and find all files we | 
 | # will compile when compiling this target. | 
 | def FindReachableFilesForConfigsInSet(target, args, | 
 |                                       files_to_check) -> set[str]: | 
 |     current_target = set() | 
 |     # Generate the project.json file, you could also specify the name with | 
 |     # --json-file-name but no real need. | 
 |     os.system("gn gen out/%s --ide=json --args='%s'" % | 
 |               (target, "\n".join(args))) | 
 |     with open('out/%s/project.json' % target) as f: | 
 |         data = json.load(f) | 
 |         for tar, values in data['targets'].items(): | 
 |             if 'sources' not in values: | 
 |                 continue | 
 |             for source in values['sources']: | 
 |                 file = source.removeprefix("//") | 
 |                 if file in files_to_check: | 
 |                     current_target.add(file) | 
 |     return current_target | 
 |  | 
 |  | 
 | # Opens every file finds the ifdef for UNSAFE_BUFFERS_BUILD and removes all | 
 | # lines from then on, until we reach the #endif. The rest of the file is | 
 | # unchanged. | 
 | # | 
 | # It is important to modify only the files in the git repository. We can use | 
 | # `git grep 'allow_unsafe_buffers'` to get the list of files. | 
 | def RemovePragmasFromFiles(files): | 
 |     for file in files: | 
 |         print("Removing opt out for: %s" % file, flush=True) | 
 |         try: | 
 |             with open(file, 'r') as f: | 
 |                 lines = f.readlines() | 
 |  | 
 |             with open(file, 'w') as f: | 
 |                 in_opt_out = False | 
 |                 for line in lines: | 
 |                     if in_opt_out: | 
 |                         if "#endif" in line: | 
 |                             in_opt_out = False | 
 |                     else: | 
 |                         if "#ifdef UNSAFE_BUFFERS_BUILD" in line: | 
 |                             in_opt_out = True | 
 |                         else: | 
 |                             f.write(line) | 
 |         except Exception as e: | 
 |             print("Failed to remove opt_out from %s: %s" % (file, str(e)), | 
 |                   flush=True) | 
 |  | 
 |  | 
 | def AddPragmasToFiles(unsafe_buffers_files, files_that_had_pragma) -> [str]: | 
 |     rewrittens = [] | 
 |     print("Unsafe buffer operations found in:", flush=True) | 
 |     for file in unsafe_buffers_files: | 
 |         print(file, flush=True) | 
 |  | 
 |     # Fix the files by inserting the opt_out_lines before the first line, | 
 |     # not starting with //. | 
 |     for file in unsafe_buffers_files: | 
 |         try: | 
 |             print("Opting out %s" % file, flush=True) | 
 |             if (not os.path.exists(file)): | 
 |                 print("File %s does not exist." % file, flush=True) | 
 |                 continue | 
 |             # If the file already had the pragma, restore the old state. | 
 |             # This prevents touching or changing too many files. | 
 |             if file in files_that_had_pragma: | 
 |                 os.system("git checkout main %s" % file) | 
 |             else: | 
 |                 with open(file, 'r') as f: | 
 |                     lines = f.readlines() | 
 |  | 
 |                 with open(file, 'w') as f: | 
 |                     inserted = False | 
 |                     for line in lines: | 
 |                         if not inserted and not line.startswith("//"): | 
 |                             for opt_out_line in opt_out_lines: | 
 |                                 f.write("%s\n" % opt_out_line) | 
 |                             inserted = True | 
 |                         f.write(line) | 
 |             rewrittens.append(file) | 
 |         except Exception as e: | 
 |             print("Failed to opt out %s: %s" % (file, str(e)), flush=True) | 
 |     return rewrittens | 
 |  | 
 |  | 
 | def AddPragmasUntilTargetCompiles(target, args, files_that_had_pragma) -> bool: | 
 |     # Configure the target. | 
 |  | 
 |     assert GenerateGnTarget(target, args), "Failed to configure target" | 
 |     no_files_rewritten = False | 
 |     while True: | 
 |         # Try building all the targets, exit if the build succeeds. | 
 |         # Do not print the output. | 
 |         if os.system("autoninja -C out/%s" % target) == 0: | 
 |             print("Build succeeded for %s." % target, flush=True) | 
 |             # Some compiles are really big clean up after ourselves. | 
 |             os.system("gn clean out/%s" % target) | 
 |             break | 
 |  | 
 |         # Clang is reporting errors likes: | 
 |         # <file>:<line>:<column>: error: unsafe pointer arithmetic [-Werror,-Wunsafe-buffer-usage] | 
 |         # | 
 |         # On Windows, this will be: | 
 |         # <file>(line,column): error: unsafe pointer arithmetic [-Werror,-Wunsafe-buffer-usage] | 
 |         # <file>:<line>:<column>: error: unsafe pointer arithmetic [-Werror,-Wunsafe-buffer-usage] | 
 |         # This is because the file is using unsafe buffer operations. | 
 |         # We will fix this by inserting the opt_out_lines in the file. | 
 |  | 
 |         # Get the list of files with unsafe buffer operations. | 
 |         unsafe_buffers_files = subprocess.check_output( | 
 |             """ | 
 |             autoninja -k 0 -C out/%s |\ | 
 |                 grep -E 'Wunsafe-buffer-usage' |\ | 
 |                 cut -d':' -f1 |\ | 
 |                 cut -d'(' -f1 |\ | 
 |                 sort -u | 
 |             """ % target, | 
 |             shell=True).decode("utf-8").split("\n") | 
 |  | 
 |         # Strip the ../../ from the file paths. | 
 |         unsafe_buffers_files = [ | 
 |             file.replace("../../", "") for file in unsafe_buffers_files | 
 |         ] | 
 |  | 
 |         # Clean empty strings. | 
 |         unsafe_buffers_files = [file for file in unsafe_buffers_files if file] | 
 |  | 
 |         rewrittens = AddPragmasToFiles(unsafe_buffers_files, | 
 |                                        files_that_had_pragma) | 
 |  | 
 |         if not rewrittens: | 
 |             print("No files were fixed.", flush=True) | 
 |             if no_files_rewritten: | 
 |                 # Don't stop the whole script but report an error so someone | 
 |                 # can check if there is a fix needed  to get this compiling. | 
 |                 # Perhaps this was a bad git commit. | 
 |                 print("Two unsuccessful builds in a row without opt-outs: %s" % | 
 |                       target) | 
 |                 break | 
 |             else: | 
 |                 no_files_rewritten = True | 
 |                 continue | 
 |         no_files_rewritten = False | 
 |  | 
 |  | 
 | def main(): | 
 |     # Collect all files that have the pragma we are interested in. | 
 |     print("Collecting files with opt_out...", flush=True) | 
 |     files_that_had_pragma = FindCodeFilesWithPragma() | 
 |     print("found %d files with pragmas." % len(files_that_had_pragma), | 
 |           flush=True) | 
 |  | 
 |     # Find all the reachable files for each gn target (this limits the removals | 
 |     # to ones we'll actually build and thus can be sure if we build properly). | 
 |     reachable_files_with_pragmas = set() | 
 |     for target, args in GnConfigs(True).all_platforms_and_configs.items(): | 
 |         print("Determining reachable files for %s" % target, flush=True) | 
 |         current_target = FindReachableFilesForConfigsInSet( | 
 |             target, args, files_that_had_pragma) | 
 |         print('target: %s has %d' % (target, len(current_target)), flush=True) | 
 |         reachable_files_with_pragmas |= current_target | 
 |  | 
 |     print("Found %d reachable files that had pragmas." % | 
 |           len(reachable_files_with_pragmas), | 
 |           flush=True) | 
 |  | 
 |     # Before adding the opt_out lines, we need to clear them in every files. | 
 |     # Note that the opt_out_lines are not very stable, the bug and comments | 
 |     # might vary. We should delete the whole block. | 
 |     RemovePragmasFromFiles(reachable_files_with_pragmas) | 
 |  | 
 |     for target, args in GnConfigs(True).all_platforms_and_configs.items(): | 
 |         print("Building for %s:" % target, flush=True) | 
 |         AddPragmasUntilTargetCompiles(target, args, files_that_had_pragma) | 
 |  | 
 |     # Once it compiles on every targets, format the code. | 
 |     os.system("git cl format") | 
 |  | 
 |     # Regenerate a couple autogen files that run into issues consistently. | 
 |     os.system("vpython3 gpu/command_buffer/build_gles2_cmd_buffer.py") | 
 |     os.system("vpython3 gpu/command_buffer/build_raster_cmd_buffer.py") | 
 |  | 
 |     # Some files are not properly covered by this script. Revert the known | 
 |     # failing files. | 
 |     exclusions = [ | 
 |         # Reproduce on android-cronet-riscv64-dbg. | 
 |         "base/profiler/register_context_registers.h", | 
 |  | 
 |         # Reproduce on linux-cast-arm-rel | 
 |         "media/parsers/h264_bit_reader.h", | 
 |     ] | 
 |     for exclusion in exclusions: | 
 |         print("Reverting %s" % exclusion, flush=True) | 
 |         os.system("git checkout HEAD -- %s" % exclusion) | 
 |  | 
 |     # Add changed code and create the commit. | 
 |     os.system("git add -u") | 
 |  | 
 |     git_commit_description =\ | 
 |         """spanification: remove `#pragma allow_unsafe_buffers` to xxx | 
 |  | 
 |         This is a clean up of any files that now compile without the pragma. | 
 |         This CL has no behavior changes. | 
 |  | 
 |         This patch was fully automated using script: | 
 |         /tools/clang/spanify/remove-unneeded-pragmas.py | 
 |  | 
 |         See internal doc about it: | 
 |         https://docs.google.com/document/d/1erdcokeh6rfBqs_h0drHqSLtbDbB61j7j3O2Pz8NH78/edit?resourcekey=0-hNe6w1hYAYyVXGEpWI7HVA&tab=t.0 | 
 |  | 
 |         Bug: 40285824""" | 
 |  | 
 |     with open("commit_description.txt", "w") as f: | 
 |         f.write(git_commit_description) | 
 |  | 
 |     os.system("git commit -F commit_description.txt --no-edit") | 
 |  | 
 |  | 
 | if __name__ == "__main__": | 
 |     sys.exit(main()) |