001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.bcel.verifier.statics;
020
021import java.util.ArrayList;
022import java.util.List;
023import java.util.Map;
024import java.util.NavigableMap;
025import java.util.TreeMap;
026
027import org.apache.bcel.generic.Type;
028import org.apache.bcel.verifier.exc.LocalVariableInfoInconsistentException;
029
030/**
031 * A utility class holding the information about the name and the type of a local variable in a given slot (== index).
032 * This information often changes in course of byte code offsets.
033 */
034public class LocalVariableInfo {
035
036    /**
037     * A contiguous, inclusive range of bytecode offsets sharing one variable name and one type.
038     */
039    private static final class Range {
040        private final int start;
041        private final int end; // inclusive
042        private final String name;
043        private final Type type;
044
045        Range(final int start, final int end, final String name, final Type type) {
046            this.start = start;
047            this.end = end;
048            this.name = name;
049            this.type = type;
050        }
051    }
052
053    /**
054     * The database of ranges, keyed by their start offset. Invariant: the stored ranges never overlap each other; additions overlapping an existing range
055     * with consistent information are coalesced into it, inconsistent ones are rejected. Storing ranges instead of one entry per offset keeps the work and
056     * memory proportional to the number of LocalVariableTable entries: the startPc and length fields are attacker-controlled in a malicious class file and
057     * would otherwise amplify each 10-byte table entry into up to 65,536 hashtable operations (CWE-407).
058     */
059    private final NavigableMap<Integer, Range> ranges = new TreeMap<>();
060
061    /**
062     * Constructs a new LocalVariableInfo.
063     */
064    public LocalVariableInfo() {
065    }
066
067    /**
068     * Adds some information about this local variable (slot).
069     *
070     * @param name variable name.
071     * @param startPc Range in which the variable is valid.
072     * @param length length of ...
073     * @param type variable type.
074     * @throws LocalVariableInfoInconsistentException Thrown if the new information conflicts with already gathered information.
075     */
076    public void add(final String name, final int startPc, final int length, final Type type) throws LocalVariableInfoInconsistentException {
077        final int endPc = startPc + length; // incl/incl-notation!
078        int mergedStart = startPc;
079        int mergedEnd = endPc;
080        // Only ranges starting at or before endPc can overlap [startPc, endPc]; since stored ranges never overlap each other, the first candidate is the
081        // last range starting at or before startPc.
082        Integer from = ranges.floorKey(startPc);
083        if (from == null) {
084            from = Integer.valueOf(startPc);
085        }
086        final List<Integer> merged = new ArrayList<>();
087        for (final Map.Entry<Integer, Range> entry : ranges.subMap(from, true, Integer.valueOf(endPc), true).entrySet()) {
088            final Range range = entry.getValue();
089            if (range.end < startPc) {
090                continue; // does not overlap.
091            }
092            final int offset = Math.max(startPc, range.start);
093            if (!range.name.equals(name)) {
094                throw new LocalVariableInfoInconsistentException(
095                    "At bytecode offset '" + offset + "' a local variable has two different names: '" + range.name + "' and '" + name + "'.");
096            }
097            if (!range.type.equals(type)) {
098                throw new LocalVariableInfoInconsistentException(
099                    "At bytecode offset '" + offset + "' a local variable has two different types: '" + range.type + "' and '" + type + "'.");
100            }
101            // Consistent overlap: coalesce, so the database stays proportional to the number of disjoint ranges.
102            mergedStart = Math.min(mergedStart, range.start);
103            mergedEnd = Math.max(mergedEnd, range.end);
104            merged.add(entry.getKey());
105        }
106        merged.forEach(ranges::remove);
107        ranges.put(Integer.valueOf(mergedStart), new Range(mergedStart, mergedEnd, name, type));
108    }
109
110    /**
111     * Returns the name of the local variable that uses this local variable slot at the given bytecode offset. Care for
112     * legal bytecode offsets yourself, otherwise the return value might be wrong. May return 'null' if nothing is known
113     * about the type of this local variable slot at the given bytecode offset.
114     *
115     * @param offset bytecode offset.
116     * @return The name of the local variable that uses this local variable slot at the given bytecode offset.
117     */
118    public String getName(final int offset) {
119        final Range range = lookup(offset);
120        return range != null ? range.name : null;
121    }
122
123    /**
124     * Returns the type of the local variable that uses this local variable slot at the given bytecode offset. Care for
125     * legal bytecode offsets yourself, otherwise the return value might be wrong. May return 'null' if nothing is known
126     * about the type of this local variable slot at the given bytecode offset.
127     *
128     * @param offset bytecode offset.
129     * @return The type of the local variable that uses this local variable slot at the given bytecode offset.
130     */
131    public Type getType(final int offset) {
132        final Range range = lookup(offset);
133        return range != null ? range.type : null;
134    }
135
136    /**
137     * Returns the range covering the given bytecode offset, or {@code null} if no range covers it. Since the stored ranges never overlap, only the range
138     * with the greatest start offset at or below the given offset can cover it.
139     */
140    private Range lookup(final int offset) {
141        final Map.Entry<Integer, Range> entry = ranges.floorEntry(Integer.valueOf(offset));
142        return entry != null && entry.getValue().end >= offset ? entry.getValue() : null;
143    }
144}