Cortys / unsafe-go-classifier

Automatic classification of "unsafe" usages in Go programs
MIT License
2 stars 0 forks source link

unsafe-go-classifier

Classification models for unsafe usages in Go code. The performance of the models is summarized in the following table: Results Table The best mean values for each metric in a given feature subset block are set in boldface. The best and worst mean values for each metric for a given model are highlighted in blue and red, respectively. A result is considered to be the best/worst within a dimension if it has the highest/lowest mean or if the null hypothesis of a one-sided fold-wise paired t-test comparing it with the highest/lowest mean result cannot be rejected with a 90% confidence.

Contents

1. Dev Environment

Use the run.sh script to start a fully-featured development container. It contains a CUDA-enabled Tensorflow installation, a JupyterLab server (at port 8888) along with all other required dependencies.

To fix the login token of the Jupyter server, you can either set the JUPYTER_TOKEN environment variable or simply put the token in a file of the same name in the root directory of this project. The Jupyter port can be changed via the JUPYTER_PORT variable.

The attach_shell.sh script can be used to start a bash shell in the running dev container. If root is passed as the first argument, a root shell is started.

2. Running evaluations

To run the experiments, simply run the evaluate.sh script. By default it will evaluate multiple combinations of models and dataset representations. The evaluated configurations can be restricted via a number of command line args. Run ./evaluate.sh --help to get more information about them.

After running the evaluation, the results can be inspected by starting an MLFlow webserver via start_mlflow.sh, or by querying the results SQLite database located at mlflow.db.

3. Perform predictions on new data

After running the evaluations, as described in the previous section, the trained models are all serialized to disk (can be found in the mlruns directory). The best performing of those models can be selected for inclusion in a standalone prediction container by running ./evaluate.sh --export --model [MODEL_NAME] [...optional other filters]. This will create an exported_models directory which contains the selected models (multiple models can be selected by passing more than one --model option). Note that the evaluation of the selected models must be completed before trying to export them.

You can either get the pre-build prediction container or build the prediction container by yourself.

Get the Prediction Container

Either use docker pull ghcr.io/cortys/usgoc/pred:latest to get the latest prediction container or build the prediction container from scratch.

To build the prediction container:

  1. Ensure that the folder exported_models within this directory is present and has the models that should be used for prediction. If the folder is empty, download the folder exported_models from the existing Docker image into the folder exported_models within this directory,
$ mkdir exported_models
$ docker pull ghcr.io/cortys/usgoc/pred:latest
$ docker cp $(docker create --name tc ghcr.io/cortys/usgoc/pred:latest):/app/exported_models ./exported_models && docker rm tc

Verify that the folder exported_models contains a folder named atomic_blocks_v127_d127_f127_p127 and the file target_label_dims.json.

  1. Ensure that the folder git submodule is updated. If the folder unsafe_go_tools is empty, update the git submodule, and
$ cd unsafe_go_tools
$ git submodule update --init --recursive
  1. Run the script to build the container: $ ./build_pred.sh The container will be tagged usgoc/pred:latest.

Run the Prediction Container

Before running the prediction container, two Docker volumes should be created: go_mod and go_cache. They will be used to persist Go dependencies between runs. The prediction container additionally needs access to the Go projects for which predictions should be produced. Set the PROJECTS_DIR environment variable to the absolute path of the directory which contains the projects that will be analyzed; this directory will be automatically mounted by the prediction script.

The prediction container can be run via ./predict.sh [global opts] [cmd] [cmd opts]. It takes the following arguments:

Examples

We will now use the following use of unsafe in the apm-agent-go library, to illustrate how the prediction container can be used:

func (t *Tracer) updateInstrumentationConfig(f func(cfg *instrumentationConfig)) {
    for {
        oldConfig := t.instrumentationConfig()
        newConfig := *oldConfig
        f(&newConfig)
        if atomic.CompareAndSwapPointer(
            (*unsafe.Pointer)(unsafe.Pointer(&t.instrumentationConfigInternal)),
            unsafe.Pointer(oldConfig), // <- We want to classify this usage.
            unsafe.Pointer(&newConfig),
        ) {
            return
        }
    }
}

We begin by visualizing the CFG that is created for this usage:

./predict.sh \
  --project elastic/beats --package go.elastic.co/apm --file config.go \
  --line 413 --snippet "unsafe.Pointer(oldConfig)," \
  show -f dot \
| dot -Tsvg | display

This only works if Graphviz (for dot) and ImageMagick (for display) are installed on the host system.

The unsafe usage can be classified as follows:

./predict.sh \
  --project elastic/beats --package go.elastic.co/apm --file config.go \
  --line 413 --snippet "unsafe.Pointer(oldConfig)," \
  predict -m WL2GNN -a 0.1 --feature-importance-scores 1 \
| jq

Prediction output for both labels (exact probabilites might vary):

{
  "probabilities": [{
    "cast-basic": 0.000799796252977103,
    "cast-bytes": 0.00023943622363731265,
    "cast-header": 0.0008311063284054399,
    "cast-pointer": 0.00024363627017010003,
    "cast-struct": 0.0023890091106295586,
    "definition": 0.0012677970807999372,
    "delegate": 0.9921323657035828,
    "memory-access": 0.001111199613660574,
    "pointer-arithmetic": 0.0008071911288425326,
    "syscall": 9.69868924585171e-05,
    "unused": 8.147588232532144e-05
  }, {
    "atomic": 0.9911662936210632,
    "efficiency": 0.00020463968394324183,
    "ffi": 0.003083886345848441,
    "generics": 0.0015664942329749465,
    "hide-escape": 0.0027959353756159544,
    "layout": 0.0004991954774595797,
    "no-gc": 6.399328412953764e-05,
    "reflect": 4.1643997974460945e-05,
    "serialization": 0.0004356006102170795,
    "types": 9.241054794983938e-05,
    "unused": 4.988365981262177e-05
  }],
  "conformal_sets": [
    ["delegate"],
    ["atomic"]
  ],
  "feature_importance_scores": [{
    // ...other labels omitted
    "delegate": [
      { "feature": ["function", ""], "importance": 1.8111777305603027 },
      { "feature": ["datatype_flag", "Pointer"], "importance": -2.739483118057251 }
    ], // ...other labels omitted
  }, {
    "atomic": [
      { "feature": ["package", "sync/atomic"], "importance": 1.2248451709747314 },
      { "feature": ["datatype_flag", "Pointer"], "importance": -0.8520904183387756 }
    ], // ...other labels omitted
  }]
}

jq is of course optional here. Note that the output format would differ if --conformal-alpha 0 (-a 0) and --feature-importance-scores 0 (the default) was used; in that case, no conformal sets and no feature importances would be produced, then the resulting JSON would only contain the two probability maps (i.e. the 2-element array at probabilites in the above example output).