October 24, 2026
Smali By bithowl: Chapter 16 Return Instructions
βEvery Method Has an Exit. What It Sends Back Matters.β

By bithowl
12 min read
In the previous chapter, we explored move instructions.
We learned how Smali moves data between registers using:
- move β copy ordinary values
- move-object β copy object references
- move-wide β copy wide values
- move-result β retrieve a method's return value
- move-exception β capture a caught exception
But we left one important question unanswered:
Who decides what a method sends back when it finishes?
Imagine you walk into a bank and ask, "Is this account authorized to make a transfer?"
The employee checks the account and gives you an answer:
- Yes, the transfer is authorized.
- No, the transfer is denied.
That answer goes back to the person who asked the question. The caller then decides what to do with it.
Android methods work in a similar way.
A method can finish without returning a value, return an integer, return an object, or return a wide value such as a long.
In Smali, those actions are represented by return instructions.
In this chapter, we'll explore four important instructions:
- return-void
- return
- return-object
- return-wide
We'll also investigate how return values influence authentication, authorization, token handling, and other security-sensitive application behaviour.
# The Story (Hook)
Imagine an Android app as a large office building. Each method is an employee working in a separate department.
One employee checks a password. Another retrieves a session token. Another calculates an account balance. Another decides whether a user can access a restricted screen.
Eventually, each employee finishes their task. But they don't all return the same kind of answer. One employee simply finishes the task. Another returns a number. Another hands back a document. Another returns a large numeric value.
In Smali, the return instruction tells the runtime what kind of value is being returned from the current method.
Think of the four instructions as four different exits:
return-void β Finish without a value
return β Return a single-register value
return-object β Return an object reference
return-wide β Return a wide value
And here's the important part:
The caller may use that returned value to decide what happens next.
That is where return instructions become especially interesting for bug hunters.
1. What Is a Return Instruction?
A return instruction ends the execution of the current method and, when applicable, supplies a value to the caller.
Consider this Java code:
int getAge() {
return 21;
}
A simplified Smali representation might look like this:
.method public getAge()I
.locals 1
const/16 v0, 0x15
return v0
.end method
Let's understand the important parts.
- .method begins the method definition.
- getAge()I describes a method named getAge that returns an integer.
- const/16 places the value 21 into v0.
- return v0 returns that value to the caller.
- .end method marks the end of the method definition.
Notice the method descriptor:
getAge()I
The I means the method returns an integer.
The return instruction must be compatible with the method's declared return type.
For example:
Remember that Smali uses register types and method descriptors to represent these values. The instruction is not chosen randomly.
2. return-void β Finish Without Returning a Value
Let's start with the simplest instruction.
return-void
This instruction ends a method that returns no value.
In Java, this usually corresponds to a method declared with void.
Java example
void showMessage() {
System.out.println("Hello, Android!");
}
Smali example
.method public showMessage()V
.locals 1
const-string v0, "Hello, Android!"
invoke-static {v0}, Ljava/lang/System;->out:Ljava/io/PrintStream;
return-void
.end method
This is an illustrative example of the return instruction; the exact compiled sequence for printing a message may differ.
The key detail is the method descriptor:
showMessage()V
The V means void.
The method performs its work and finishes without returning a value.
A simpler example
.method public logout()V
.locals 0
invoke-static {}, Lcom/example/SessionManager;->clearSession()V
return-void
.end method
Here, logout() calls another method to clear the session and then returns to its caller.
The return-void instruction does not itself clear the session. That behaviour belongs to the method being called.
Bug Hunter Perspective
When auditing logout functionality, finding return-void tells you where this method finishes. It doesn't prove that logout is secure.
Investigate the instructions before the return:
- Is the session token actually cleared?
- Are cached credentials removed where necessary?
- Is the server-side session invalidated when the design requires it?
- Can a previously authenticated session still access protected functionality?
The return instruction marks the exit. The security question is whether the method performed the necessary work before reaching it.
3. return β Returning a Single-Register Value
The next instruction is: return v0
It returns a single-register value to the caller.
This instruction is commonly used for primitive values such as:
- int
- boolean
- float
- Other supported single-register primitive types
Java example
boolean isLoggedIn() {
return true;
}
Smali example
.method public isLoggedIn()Z
.locals 1
const/4 v0, 0x1
return v0
.end method
Let's break it down.
- isLoggedIn()Z declares a method that returns a boolean.
- const/4 v0, 0x1 places 1 into v0.
- return v0 returns that value.
In this context, 0x1 represents true.
For a false result, the method could use:
const/4 v0, 0x0
return v0
The method's declared return type is still boolean; only the returned value changes.
Returning a security decision
Imagine a method checks whether a user is an administrator.
Java:
boolean isAdmin(User user) {
return user.hasAdminRole();
}
A simplified Smali pattern might look like this:
invoke-virtual {p1}, Lcom/example/User;->hasAdminRole()Z
move-result v0
return v0
The method call returns a boolean. move-result captures that result, and return sends it back to the caller.
The flow is:
User.hasAdminRole()
|
v
move-result v0
|
v
return v0
|
v
Caller
This pattern is useful when tracing permission checks and authentication decisions.
Bug Hunter Perspective: The Meaning of a Boolean
Suppose you find:
.method public isPremiumUser()Z
.locals 1
const/4 v0, 0x1
return v0
.end method
This method returns true unconditionally along this code path.
That is worth investigating, but don't stop there.
Ask:
- Is this the actual method used to enforce premium access?
- Is it test code, a demo implementation, or a legitimate constant result?
- Does the server independently enforce subscription status?
- Can an untrusted client modify or bypass the local decision?
- Are protected operations accessible without a server-side authorization check?
A constant true in a method named isPremiumUser() may be suspicious. Its actual security impact depends on how the result is used and whether any authoritative controls exist elsewhere.
Important lesson: Returning true is not automatically a vulnerability. Returning an untrusted authorization decision and relying on it to protect a sensitive operation can be.
4. return-object β Returning an Object Reference
Now imagine a method retrieves a session token. The token is a String, which is an object in Java.
To return an object reference, Smali uses:
return-object v0
Java example
String getToken() {
return "sample-token";
}
Smali example
.method public getToken()Ljava/lang/String;
.locals 1
const-string v0, "sample-token"
return-object v0
.end method
Let's decode the descriptor:
getToken()Ljava/lang/String;
- getToken is the method name.
- () means there are no explicit parameters.
- Ljava/lang/String; is the return type.
The method places a string reference into v0 and returns that reference to the caller.
Returning a field
Remember our previous chapter about fields?
A method may read an object reference from a field and return it.
For example:
String getToken() {
return currentToken;
}
A simplified Smali pattern might be:
.method public getToken()Ljava/lang/String;
.locals 1
iget-object v0, p0, Lcom/example/Session;->currentToken:Ljava/lang/String;
return-object v0
.end method
Here:
- iget-object reads the instance field.
- v0 receives the object reference.
- return-object returns that reference.
This gives a bug hunter a clear path to investigate:
Session.currentToken
|
v
iget-object
|
v
v0
|
v
return-object
|
v
Caller
The next question is: who receives the token, and what happens to it?
Returning an object created elsewhere
A method might also return an object produced by another method:
invoke-virtual {p0}, Lcom/example/Session;->loadToken()Ljava/lang/String;
move-result-object v0
return-object v0
The returned reference is obtained from loadToken() and then passed back to the original caller.
Notice the difference between these instructions:
- move-result-object captures the return value of a method invocation.
- return-object returns an object reference from the current method.
They are related, but they are not interchangeable.
Bug Hunter Perspective: Follow Sensitive Objects
When you encounter return-object, pay attention to what is being returned.
Useful targets include:
- Authentication tokens
- User profile objects
- Account or payment objects
- URLs
- WebView-related objects
- Cryptographic objects
- Objects containing permissions or roles
For example, if a method returns a token, trace that reference into the caller. Then follow it toward the network client, storage layer, logger, or other consumer.
Also inspect the object's origin and access controls.
A method returning a token doesn't prove the token is exposed. It tells you where the value crosses a method boundary, which can make the data flow easier to follow.
5. return-wide β Returning Wide Values
Some Java types need two register slots in Dalvik-style register instructions.
The common examples are:
- long
- double
Smali uses:
return-wide v0
Java example
long getAccountId() {
return 987654321L;
}
Smali example
.method public getAccountId()J
.locals 2
const-wide v0, 0x3ade68b1L
return-wide v0
.end method
The method descriptor ends with J, which represents long. The wide value begins at v0 and occupies the register pair v0 and v1.
The instruction return-wide v0 returns that wide value to the caller.
What about double?
A method returning a double also uses return-wide.
For example:
double getBalance() {
return 125.50;
}
The exact Smali constant representation can vary, but the return instruction remains:
return-wide v0
The method descriptor for double ends with D.
Why should bug hunters care?
Wide values can be important in security-sensitive logic involving:
- Timestamps
- Session expiration
- Account balances
- Transaction amounts
- Numeric identifiers
- Rate-limit counters
Imagine an app calculates whether a session has expired using a timestamp.
If you find a method returning a long, follow the value into the caller and inspect how the result is compared.
Does the app use the correct time units? Is the comparison logically correct? Can an attacker influence the value? Is the server enforcing session expiration independently where required?
The return instruction alone doesn't answer those questions, but it helps you locate the point where the value leaves the method.
6. How Return Instructions Connect to Method Calls
Let's connect this chapter to Chapter 15.
Consider this Java code:
boolean isAuthorized() {
return true;
}
void accessDashboard() {
if (isAuthorized()) {
openDashboard();
}
}
The first method returns a boolean. The second method receives the result and uses it in a condition.
A simplified Smali pattern might look like this:
invoke-virtual {p0}, Lcom/example/Auth;->isAuthorized()Z
move-result v0
if-eqz v0, :denied
invoke-virtual {p0}, Lcom/example/Dashboard;->openDashboard()V
:denied
return-void
Let's follow the execution.
- isAuthorized() is called.
- move-result v0 receives its boolean result.
- if-eqz v0, :denied branches to :denied if the value is zero, meaning false in this context.
- If the value is nonzero, execution continues to openDashboard().
- The method eventually reaches return-void.
This example demonstrates how a returned value can influence a control-flow decision.
But remember: the exact compiled code may contain additional checks, branches, or method calls.
Bug Hunter Perspective
This is a powerful pattern to search for during an audit:
Security method
|
v
Return value
|
v
Register
|
v
Conditional branch
|
v
Sensitive operation
When reviewing authentication or authorization:
- Find the method that produces the decision.
- Inspect how that method reaches its return instruction.
- Follow the result back to the caller.
- Inspect the conditional branch that consumes it.
- Determine whether the protected operation is genuinely guarded.
- Check whether the server enforces the relevant permission independently.
A correct-looking return instruction doesn't guarantee that the overall security decision is correct. The complete path matters.
7. A Bug Hunter Lab: Trace a Token from Source to Caller
Let's investigate a small example.
Suppose the app contains this method:
.method public getAccessToken()Ljava/lang/String;
.locals 1
iget-object v0, p0, Lcom/example/Session;->token:Ljava/lang/String;
return-object v0
.end method
And another method calls it:
invoke-virtual {p0}, Lcom/example/Session;->getAccessToken()Ljava/lang/String;
move-result-object v0
invoke-static {v0}, Lcom/example/NetworkClient;->setToken(Ljava/lang/String;)V
Let's analyze the flow.
Step 1: Identify the source
iget-object v0, p0, Lcom/example/Session;->token:Ljava/lang/String;
The method reads the token field from the Session object.
Step 2: Identify the return
return-object v0
The method returns the token reference to its caller.
Step 3: Identify the receiving register
move-result-object v0
The caller retrieves the returned reference into its own v0.
The same register name does not mean it's the same physical register across two different method invocations. Register names are local to each method's register frame.
Step 4: Identify the next consumer
invoke-static {v0}, Lcom/example/NetworkClient;->setToken(Ljava/lang/String;)V
The returned reference is passed to setToken().
Step 5: Investigate the security behaviour
Now inspect setToken() and the network operations it influences.
Ask:
- Is the token stored safely?
- Is it sent only to the intended endpoint?
- Could it be included in logs?
- Does the network client attach it to the correct requests?
- Does the server validate the token and its permissions?
You have now followed a value across a method boundary instead of examining one instruction in isolation.
8. Common Mistakes Beginners Make
Mistake 1: Confusing return with return-object
These instructions are not interchangeable.
return v0
Returns a single-register value.
return-object v0
Returns an object reference.
Use the instruction that matches the method's declared return type.
Mistake 2: Assuming return-void means the app closes
It doesn't. return-void ends the current method. The caller may continue executing immediately afterward.
An Android app can have thousands of methods, each with its own return point.
Mistake 3: Ignoring the caller
Finding a method that returns false is only half the investigation. You also need to find out whether the caller respects that result or ignores it. A returned security decision matters only in the context of how the result is used.
Mistake 4: Assuming a suspicious return proves a vulnerability
A method named isAdmin() that returns true deserves investigation, but the name alone is not proof.
It could be test code, unreachable code, a demo implementation, or a result that is overridden by authoritative server-side checks. Verify the execution path and demonstrate the impact before concluding that a vulnerability exists.
Mistake 5: Forgetting method descriptors
Before interpreting a return instruction, check the method signature.
For example:
getName()Ljava/lang/String;
returns a string reference, while:
getCount()I
returns an integer.
The descriptor helps you understand what the caller should receive.
9. Quick Reference Table
And here's a quick way to remember the family:
- Void: nothing comes back.
- Return: a single-register value comes back.
- Object: an object reference comes back.
- Wide: a wide value comes back.
10. Practical Exercises
Exercise 1: Identify the return type
.method public getCount()I
.locals 1
const/4 v0, 0x5
return v0
.end method
Questions:
- What is the method's return type?
- What value does it return?
Answer: It returns an integer with the value 5.
Exercise 2: Understand return-object
.method public getName()Ljava/lang/String;
.locals 1
const-string v0, "Alex"
return-object v0
.end method
What does the method return?
Answer: A reference to the string "Alex".
Exercise 3: Follow a boolean result
.method public isVerified()Z
.locals 1
const/4 v0, 0x0
return v0
.end method
What does the method return?
Answer: false, because 0x0 represents false in this boolean context.
Exercise 4: Understand a wide return
.method public getTimestamp()J
.locals 2
const-wide v0, 0x64L
return-wide v0
.end method
What does J represent, and which instruction returns the value?
Answer: J represents long, and return-wide v0 returns the wide value.
Exercise 5: Think like a bug hunter
Consider:
invoke-virtual {p0}, Lcom/example/Auth;->isAuthorized()Z
move-result v0
if-eqz v0, :blocked
invoke-virtual {p0}, Lcom/example/Payments;->submitPayment()V
:blocked
return-void
Investigate these questions:
- Which method produces the authorization result?
- Which instruction receives that result?
- What happens when the result is zero?
- Which operation is reached when the result is nonzero?
- What additional checks would you perform to determine whether payment authorization is secure?
Answer: isAuthorized() produces the boolean result. move-result v0 receives it. If v0 is zero, execution branches to :blocked; otherwise, it calls submitPayment(). A full audit should also inspect the authorization method, the complete control flow, and the server-side payment authorization.
11. Bug Hunter Checklist
When analysing return instructions, use this checklist:
- Identify the method's declared return type.
- Find the instruction that returns the value.
- Trace how that value was created or obtained.
- Find the caller and locate its corresponding move-result instruction when a result is returned.
- Follow the returned value through subsequent registers and branches.
- Determine whether the value controls authentication, authorization, or another sensitive operation.
- Inspect whether failure paths return safe values.
- Check whether the caller handles unexpected values correctly.
- Verify server-side enforcement for security-sensitive operations where appropriate.
- Confirm the actual security impact before classifying the behaviour as a vulnerability.
# Quick Recap
In this chapter, we learned four ways a Smali method can finish:
- return-void ends a method without returning a value.
- return returns a single-register value, such as an integer or boolean.
- return-object returns an object reference, such as a string or custom object.
- return-wide returns a wide value, such as a long or double.
We also learned how a return value travels back to the caller and can influence what happens next.
That last point is crucial.
A method might return a boolean indicating whether access is allowed. A caller might use that boolean to decide whether to open a screen or submit a transaction. A method might return a token that another method later sends over the network.
The return instruction is where a method hands its result back. The caller reveals what that result actually means.
# Final Thought
Smali analysis is not just about recognizing instructions. It's about understanding how the instructions connect.
A field supplies a value. A register carries it. A method processes it. A return instruction hands it back. Another method receives it and makes the next decision.
Once you can follow that chain, you stop seeing isolated lines of Smali and start seeing the application's logic.
And for a bug hunter, that is where the interesting discoveries begin.
By β bithowl