carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3// Verifying an untrusted GeoIP database in a child process (code review
4// R9). A .mmdb from a feed or an upload is parsed by maxminddb, which
5// trusts its metadata: a crafted file can panic it (node_count arithmetic)
6// or make its tree walk run for 2^128 steps inside a single call. Neither
7// may take the console down or hold the publish slot past its deadline, so
8// the console never parses such a file itself: it runs its own binary with
9// a hidden verb, kills it at the deadline, and reads back a small report.
10
11import (
12 "bytes"
13 "context"
14 "encoding/json"
15 "errors"
16 "fmt"
17 "io"
18 "net"
19 "os"
20 "os/exec"
21 "time"
22
23 "github.com/carlosframework/platform/internal/geo"
24)
25
26// GeoipVerifyVerb is cmd/console's hidden verb for the verifier child.
27const GeoipVerifyVerb = "__geoip-verify"
28
29// geoipVerifyResult is the child's report.
30type geoipVerifyResult struct {
31 Source string `json:"source"`
32 BuildEpoch int64 `json:"build_epoch"`
33 NodeCount uint64 `json:"node_count"`
34 Countries int `json:"countries"` // how many probe addresses answered a country
35}
36
37// RunGeoipVerify is the child: args are the file, then the probe
38// addresses. It writes the report to w and returns the exit code (0 ok, 2
39// refused). A panic or a hang here kills only this process.
40func RunGeoipVerify(args []string, w io.Writer) int {
41 if len(args) < 1 {
42 return 2
43 }
44 // Measured: DB-IP City Lite (127 MB) verifies within 256 MiB of
45 // private memory; 512 MiB leaves room for GeoLite2 and growth.
46 if err := limitVerifierMemory(geoipVerifyDataLimit); err != nil {
47 return 2 // without the bound this process must not parse anything
48 }
49 db, err := geo.OpenMMDBVerified(context.Background(), args[0])
50 if errors.Is(err, geo.ErrSearchTreeTooLarge) {
51 return geoipVerifyExitTree
52 }
53 if err != nil {
54 return 2
55 }
56 defer db.Close()
57 // Every record through the production schema, here where a pathological
58 // one can only kill this process (code review R11-1).
59 if err := db.CheckRecords(context.Background()); err != nil {
60 return 2
61 }
62 res := geoipVerifyResult{Source: db.Source(), BuildEpoch: db.BuildEpoch().Unix(), NodeCount: db.NodeCount()}
63 for _, a := range args[1:] {
64 if r, ok := db.Lookup(net.ParseIP(a)); ok && r.Country != "" {
65 res.Countries++
66 }
67 }
68 if err := json.NewEncoder(w).Encode(res); err != nil {
69 return 2
70 }
71 return 0
72}
73
74// geoipVerifyDataLimit is the child's private-memory cap (code review R10-1).
75const geoipVerifyDataLimit = 512 << 20
76
77// geoipVerifyTimeout bounds the child; a var so a test need not wait.
78var geoipVerifyTimeout = 5 * time.Minute
79
80// geoipVerifyExec starts the child; tests point it at the test binary.
81var geoipVerifyExec = func(ctx context.Context, args ...string) *exec.Cmd {
82 self, err := os.Executable()
83 if err != nil {
84 self = os.Args[0]
85 }
86 return exec.CommandContext(ctx, self, append([]string{GeoipVerifyVerb}, args...)...)
87}
88
89var (
90 errGeoipVerifyTimeout = errors.New("verification took too long")
91 errGeoipVerifyTree = errors.New("malformed search tree")
92)
93
94// geoipVerifyExitTree is the child's exit code for a search tree with far
95// more networks than nodes.
96const geoipVerifyExitTree = 3
97
98// verifyInChild runs the child and returns its report. A crash, a refusal
99// or a malformed report is an invalid database; the deadline is its own
100// category; the caller's own cancellation is returned as it is.
101func verifyInChild(ctx context.Context, path string, probes []string) (geoipVerifyResult, error) {
102 vctx, cancel := context.WithTimeout(ctx, geoipVerifyTimeout)
103 defer cancel()
104 cmd := geoipVerifyExec(vctx, append([]string{path}, probes...)...)
105 var out bytes.Buffer
106 cmd.Stdout = &cappedBuffer{buf: &out, max: 4 << 10}
107 cmd.Stderr = io.Discard
108 cmd.WaitDelay = 5 * time.Second
109 err := cmd.Run()
110 switch {
111 case ctx.Err() != nil:
112 return geoipVerifyResult{}, ctx.Err()
113 case vctx.Err() != nil:
114 return geoipVerifyResult{}, errGeoipVerifyTimeout
115 case err != nil:
116 var ee *exec.ExitError
117 if errors.As(err, &ee) && ee.ExitCode() == geoipVerifyExitTree {
118 return geoipVerifyResult{}, errGeoipVerifyTree
119 }
120 return geoipVerifyResult{}, fmt.Errorf("verifier: %w", err)
121 }
122 var res geoipVerifyResult
123 if err := json.Unmarshal(out.Bytes(), &res); err != nil {
124 return geoipVerifyResult{}, fmt.Errorf("verifier report: %w", err)
125 }
126 return res, nil
127}
128
129type cappedBuffer struct {
130 buf *bytes.Buffer
131 max int
132}
133
134func (c *cappedBuffer) Write(p []byte) (int, error) {
135 if c.buf.Len()+len(p) > c.max {
136 return 0, errors.New("verifier report too large")
137 }
138 return c.buf.Write(p)
139}
140
141// VerifyGeoIPFile checks a database in the verifier child; nil means it is
142// safe to open in this process with geo.OpenMMDBTrusted. cmd/console uses
143// it for the CONSOLE_GEOIP_DB override, so even an operator's broken file
144// cannot crash the console at startup.
145func VerifyGeoIPFile(ctx context.Context, path string) error {
146 _, err := verifyInChild(ctx, path, nil)
147 return err
148}
149